diff options
Diffstat (limited to 'toolkit/content/tests')
388 files changed, 45369 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.toml b/toolkit/content/tests/browser/browser.toml new file mode 100644 index 0000000000..3f668b39d7 --- /dev/null +++ b/toolkit/content/tests/browser/browser.toml @@ -0,0 +1,201 @@ +[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", +] + +["browser_about_logging.js"] +skip-if = ["tsan"] # Bug 1804081 + +["browser_about_networking.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked #category-dns may not be focusable +skip-if = ["socketprocess_networking"] + +["browser_autoscroll_disabled.js"] +skip-if = ["true"] # Bug 1312652 + +["browser_autoscroll_disabled_on_editable_content.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked browser may not be focusable + +["browser_autoscroll_disabled_on_links.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked browser may not be focusable + +["browser_bug295977_autoscroll_overflow.js"] +skip-if = [ + "os == 'win' && bits == 64 && (debug || asan)", + "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_bug1170531.js"] +skip-if = ["os == 'linux' && !debug && !ccov"] # Bug 1647973 + +["browser_bug1198465.js"] + +["browser_bug1572798.js"] +tags = "audiochannel" +support-files = ["file_document_open_audio.html"] + +["browser_bug1693577.js"] + +["browser_cancel_starting_autoscrolling_requested_by_background_tab.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked tabbrowser-tabpanels may not be accessible + +["browser_charsetMenu_disable_on_ascii.js"] + +["browser_charsetMenu_swapBrowsers.js"] + +["browser_click_event_during_autoscrolling.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked browser may not be accessible + +["browser_contentTitle.js"] + +["browser_content_url_annotation.js"] +run-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" + +["browser_delay_autoplay_media_pausedAfterPlay.js"] +tags = "audiochannel" + +["browser_delay_autoplay_multipleMedia.js"] +tags = "audiochannel" + +["browser_delay_autoplay_notInTreeAudio.js"] +tags = "audiochannel" + +["browser_delay_autoplay_playAfterTabVisible.js"] +tags = "audiochannel" + +["browser_delay_autoplay_playMediaInMuteTab.js"] +tags = "audiochannel" + +["browser_delay_autoplay_silentAudioTrack_media.js"] +tags = "audiochannel" +skip-if = [ + "os == 'mac'", # Bug 1524746 + "os == 'linux' && !debug", # Bug 1524746 +] + +["browser_delay_autoplay_webAudio.js"] +tags = "audiochannel" + +["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"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked label#textlink-text may not be focusable +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 = ["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 = ["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"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked body.wide-container may not be accessible + +["browser_suspend_videos_outside_viewport.js"] +support-files = [ + "file_outside_viewport_videos.html", + "gizmo.mp4", +] 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..6ab58a7c48 --- /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.startLoadingURIString(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..c1c89e3799 --- /dev/null +++ b/toolkit/content/tests/browser/browser_autoscroll_disabled_on_links.js @@ -0,0 +1,124 @@ +/* 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 => { + 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..0d471890c3 --- /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.startLoadingURIString( + 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.startLoadingURIString( + 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.startLoadingURIString( + 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.startLoadingURIString(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..d14b9afd6a --- /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.startLoadingURIString( + 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..b1cdbfa62c --- /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.startLoadingURIString(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..ffbc916a5e --- /dev/null +++ b/toolkit/content/tests/browser/browser_bug982298.js @@ -0,0 +1,79 @@ +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); + + Assert.equal( + 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.startLoadingURIString(browser, "about:blank"); + await BrowserTestUtils.browserLoaded(browser); + + Assert.equal( + 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..3674926a5a --- /dev/null +++ b/toolkit/content/tests/browser/browser_cancel_starting_autoscrolling_requested_by_background_tab.js @@ -0,0 +1,155 @@ +/* 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 => { + 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..e480a531e5 --- /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.startLoadingURIString(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..e4613b5da5 --- /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.startLoadingURIString( + 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..b369f00a08 --- /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.startLoadingURIString(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..84d021a10a --- /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.startLoadingURIString(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..578d4c1496 --- /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.startLoadingURIString(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.startLoadingURIString(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..4289c1fec3 --- /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.startLoadingURIString(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..144c9dfdc1 --- /dev/null +++ b/toolkit/content/tests/browser/browser_findbar.js @@ -0,0 +1,564 @@ +/* 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); + Assert.strictEqual( + 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); + Assert.strictEqual( + 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); + Assert.strictEqual( + 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.startLoadingURIString(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); + Assert.strictEqual( + 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; + } + ); + + Assert.greater(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..bd025086f7 --- /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"); + Assert.greater(resizedValues[1] - 50, values[1], "second value"); + Assert.greater(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..21b22fe171 --- /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.startLoadingURIString( + 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.startLoadingURIString(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..a1cc42dd97 --- /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.startLoadingURIString(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..4b46fd9351 --- /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-background-video.enabled", true], + ["media.suspend-background-video.delay-ms", 0], + ["media.resume-background-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.startLoadingURIString(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..ce45d04fdc --- /dev/null +++ b/toolkit/content/tests/browser/browser_save_folder_standalone_image.js @@ -0,0 +1,79 @@ +/* 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 dir = FileUtils.getDir("TmpD", ["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", []); + 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..5eb1b1c904 --- /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.startLoadingURIString( + 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..083b6a13a0 --- /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; + Assert.notEqual(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..e36f1c75b8 --- /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-background-video.enabled", true], + ["media.suspend-background-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.toml b/toolkit/content/tests/browser/datetime/browser.toml new file mode 100644 index 0000000000..6e8580ddc4 --- /dev/null +++ b/toolkit/content/tests/browser/datetime/browser.toml @@ -0,0 +1,96 @@ +[DEFAULT] +support-files = ["head.js"] + +["browser_datetime_blur.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked td.outside may not be accessible +# 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", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_clear.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_focus.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_keynav.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_markup.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_min_max.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked TD may not be accessible +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "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", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_datepicker_mousenav.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked td.weekend.outside may not be accessible +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "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", # fails on asan + "os == 'linux' && fission && socketprocess_networking && !debug", # high frequency intermittent, Bug 1673140 +] + +["browser_datetime_showPicker.js"] +# do not skip + +["browser_datetime_toplevel.js"] +fail-if = ["a11y_checks"] # Bug 1854538 clicked input may not be accessible + +["browser_spinner.js"] +skip-if = [ + "tsan", # Frequently times out on TSan + "os == 'win' && asan", # fails on asan + "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", # fails on asan + "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..e7ac0037b9 --- /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.isVisible(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..b99e8ed0e8 --- /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.isHidden(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..efa2fbfeab --- /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.isHidden(helper.getElement(MONTH_YEAR_VIEW)), + "Month-year selection spinner is not visible" + ); + Assert.ok( + BrowserTestUtils.isHidden(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.isVisible(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.isVisible(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.isHidden(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..e722f883d5 --- /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.isVisible(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.isHidden(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.isVisible(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.isHidden(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..bbef72873c --- /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.isVisible(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.isHidden(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..be15cd9684 --- /dev/null +++ b/toolkit/content/tests/browser/head.js @@ -0,0 +1,252 @@ +"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; + } + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + BrowserTestUtils.waitForMutationCondition( + findbar, + { attributes: true, attributeFilter: ["hidden"] }, + () => findbar.hidden + ).then(resolve); + } else { + 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 diff --git a/toolkit/content/tests/chrome/RegisterUnregisterChrome.js b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js new file mode 100644 index 0000000000..26e4e80922 --- /dev/null +++ b/toolkit/content/tests/chrome/RegisterUnregisterChrome.js @@ -0,0 +1,141 @@ +/* This code is mostly copied from chrome/test/unit/head_crtestutils.js */ + +// This file assumes chrome-harness.js is loaded in the global scope. +/* import-globals-from ../../../../testing/mochitest/chrome-harness.js */ + +const NS_CHROME_MANIFESTS_FILE_LIST = "ChromeML"; +const XUL_CACHE_PREF = "nglayout.debug.disable_xul_cache"; + +var gChromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService( + Ci.nsIXULChromeRegistry +); + +// Create the temporary file in the profile, instead of in TmpD, because +// we know the mochitest harness kills off the profile when it's done. +function copyToTemporaryFile(f) { + let tmpd = Services.dirsvc.get("ProfD", Ci.nsIFile); + let tmpf = tmpd.clone(); + tmpf.append("temp.manifest"); + tmpf.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + tmpf.remove(false); + f.copyTo(tmpd, tmpf.leafName); + return tmpf; +} + +function* dirIter(directory) { + var testsDir = Services.io + .newURI(directory) + .QueryInterface(Ci.nsIFileURL).file; + + let en = testsDir.directoryEntries; + while (en.hasMoreElements()) { + yield en.nextFile; + } +} + +function getParent(path) { + let lastSlash = path.lastIndexOf("/"); + if (lastSlash == -1) { + lastSlash = path.lastIndexOf("\\"); + if (lastSlash == -1) { + return ""; + } + return "/" + path.substring(0, lastSlash).replace(/\\/g, "/"); + } + return path.substring(0, lastSlash); +} + +function copyDirToTempProfile(path, subdirname) { + if (subdirname === undefined) { + subdirname = "mochikit-tmp"; + } + + let tmpdir = Services.dirsvc.get("ProfD", Ci.nsIFile); + tmpdir.append(subdirname); + tmpdir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o777); + + let rootDir = getParent(path); + if (rootDir == "") { + return tmpdir; + } + + // The SimpleTest directory is hidden + var files = Array.from(dirIter("file://" + rootDir)); + for (let f in files) { + files[f].copyTo(tmpdir, ""); + } + return tmpdir; +} + +function convertChromeURI(chromeURI) { + let uri = Services.io.newURI(chromeURI); + return gChromeReg.convertChromeURL(uri); +} + +function chromeURIToFile(chromeURI) { + var jar = getJar(chromeURI); + if (jar) { + var tmpDir = extractJarToTmp(jar); + let parts = chromeURI.split("/"); + if (parts[parts.length - 1] != "") { + tmpDir.append(parts[parts.length - 1]); + } + return tmpDir; + } + + return convertChromeURI(chromeURI).QueryInterface(Ci.nsIFileURL).file; +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function createManifestTemporarily(tempDir, manifestText) { + Services.prefs.setBoolPref(XUL_CACHE_PREF, true); + + tempDir.append("temp.manifest"); + + let foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + foStream.init(tempDir, 0x02 | 0x08 | 0x20, 0o664, 0); // write, create, truncate + foStream.write(manifestText, manifestText.length); + foStream.close(); + let tempfile = copyToTemporaryFile(tempDir); + + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(tempfile); + + return function () { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + Services.prefs.clearUserPref(XUL_CACHE_PREF); + }; +} + +// Register a chrome manifest temporarily and return a function which un-does +// the registrarion when no longer needed. +function registerManifestTemporarily(manifestURI) { + Services.prefs.setBoolPref(XUL_CACHE_PREF, true); + + let file = chromeURIToFile(manifestURI); + + let tempfile = copyToTemporaryFile(file); + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(tempfile); + + return function () { + tempfile.fileSize = 0; // truncate the manifest + gChromeReg.checkForNewChrome(); + Services.prefs.clearUserPref(XUL_CACHE_PREF); + }; +} + +function registerManifestPermanently(manifestURI) { + var chromepath = chromeURIToFile(manifestURI); + + Components.manager + .QueryInterface(Ci.nsIComponentRegistrar) + .autoRegister(chromepath); + return chromepath; +} diff --git a/toolkit/content/tests/chrome/bug263683_window.xhtml b/toolkit/content/tests/chrome/bug263683_window.xhtml new file mode 100644 index 0000000000..b124361fd7 --- /dev/null +++ b/toolkit/content/tests/chrome/bug263683_window.xhtml @@ -0,0 +1,206 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="263683test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="263683 test"> + + <script type="application/javascript"><![CDATA[ + const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var info = window.arguments[0].info; + var is = window.arguments[0].is; + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + // Testing on a remote browser has been disabled due to frequent + // intermittent failures. + for (let browserId of ["content"/*, "content-remote"*/]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + // We're bailing out when testing a remote browser on OSX 10.6, because it + // fails permanently. + if (browserId.endsWith("remote") && AppConstants.isPlatformAndVersionAtMost("macosx", 11)) { + return; + } + + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, 'data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + await promise; + await onDocumentLoaded(); + } + + function toggleHighlightAndWait(highlight) { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + gFindBar.toggleHighlight(highlight); + }); + } + + async function onDocumentLoaded() { + gFindBar.open(); + var search = "mozilla"; + gFindBar._findField.focus(); + gFindBar._findField.value = search; + var matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + let promise = toggleHighlightAndWait(true); + gFindBar._find(); + await promise; + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + Assert.ok("SELECTION_FIND" in controller, "Correctly detects new selection type"); + let selection = controller.getSelection(controller.SELECTION_FIND); + + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + let input = content.document.getElementById("inp"); + input.value = args.search; + }); + + await toggleHighlightAndWait(true); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 1, + "Correctly added a match from input to the selection type"); + Assert.equal(inputSelection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let input = content.document.getElementById("inp"); + let inputController = input.editor.selectionController; + let inputSelection = inputController.getSelection(inputController.SELECTION_FIND); + + Assert.equal(inputSelection.rangeCount, 0, "Correctly removed the range"); + }); + + // For posterity, test iframes too. + + promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, 'data:text/html,<h2>Text mozilla</h2><iframe id="leframe" ' + + 'src="data:text/html,Text mozilla"></iframe>'); + await promise; + + await toggleHighlightAndWait(true); + + await SpecialPowers.spawn(gBrowser, [{ search }], async function(args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow.docShell); + Assert.equal(selection.rangeCount, 1, + "Correctly added a match to the selection type"); + Assert.equal(selection.getRangeAt(0).toString().toLowerCase(), + args.search, "Added the correct match"); + }); + + await toggleHighlightAndWait(false); + + const matches = JSON.parse(gFindBar._foundMatches.dataset.l10nArgs); + is(matches.total, 2, "Found correct amount of matches") + + await SpecialPowers.spawn(gBrowser, [], async function(args) { + function getSelection(docShell) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller.getSelection(controller.SELECTION_FIND); + } + + let selection = getSelection(docShell); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + // Check the iframe too: + let frame = content.document.getElementById("leframe"); + // Hoops! Get the docShell first, then the selection. + selection = getSelection(frame.contentWindow.docShell); + Assert.equal(selection.rangeCount, 0, "Correctly removed the range"); + + content.document.documentElement.focus(); + }); + + gFindBar.close(true); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug304188_window.xhtml b/toolkit/content/tests/chrome/bug304188_window.xhtml new file mode 100644 index 0000000000..11343f2ace --- /dev/null +++ b/toolkit/content/tests/chrome/bug304188_window.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="FindbarTest for bug 304188 - +find-menu appears in editor element which has had makeEditable() called but designMode not set"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + const {ContentTask} = ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + ContentTask.setTestScope(window.arguments[0]); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var info = window.arguments[0].info; + var ok = window.arguments[0].ok; + + function onLoad() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = ContentTask.spawn(gBrowser, [], async function() { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }); + }); + BrowserTestUtils.startLoadingURIString(gBrowser, "data:text/html;charset=utf-8,some%20random%20text"); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + await ContentTask.spawn(gBrowser, [], async function() { + var edsession = content.docShell.editingSession; + edsession.makeWindowEditable(content, "html", false, true, false); + content.focus(); + }); + + await enterStringIntoEditor("'"); + await enterStringIntoEditor("/"); + + ok(gFindBar.hidden, + "Findfield should have stayed hidden after entering editor test"); + } + + async function enterStringIntoEditor(aString) { + for (let i = 0; i < aString.length; i++) { + await ContentTask.spawn(gBrowser, [{ charCode: aString.charCodeAt(i) }], async function(args) { + let event = new content.window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: args.charCode, + }); + content.document.body.dispatchEvent(event); + }); + } + } + ]]></script> + + <browser id="content" flex="1" src="about:blank" type="content" primary="true" messagemanagergroup="test"/> + <browser id="content-remote" remote="true" flex="1" src="about:blank" type="content" primary="true" messagemanagergroup="test"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug331215_window.xhtml b/toolkit/content/tests/chrome/bug331215_window.xhtml new file mode 100644 index 0000000000..20c08dbd66 --- /dev/null +++ b/toolkit/content/tests/chrome/bug331215_window.xhtml @@ -0,0 +1,99 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="331215test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="331215 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var info = window.arguments[0].info; + var ok = window.arguments[0].ok; + SimpleTest.requestLongerTimeout(2); + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, "data:text/plain,latest"); + await promise; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + document.getElementById("cmd_find").doCommand(); + await promiseEnterStringIntoFindField("test"); + document.commandDispatcher + .getControllerForCommand("cmd_moveTop") + .doCommand("cmd_moveTop"); + await promiseEnterStringIntoFindField("l"); + ok(gFindBar._findField.getAttribute("status") == "notfound", + "Findfield status attribute should have been 'notfound' after entering test"); + await promiseEnterStringIntoFindField("a"); + ok(gFindBar._findField.getAttribute("status") != "notfound", + "Findfield status attribute should not have been 'notfound' after entering latest"); + } + + function promiseEnterStringIntoFindField(aString) { + return new Promise(resolve => { + let listener = { + onFindResult(result) { + if (result.result == Ci.nsITypeAheadFind.FIND_FOUND && result.searchString != aString) + return; + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + + for (let c of aString) { + let code = c.charCodeAt(0); + let ev = new KeyboardEvent("keypress", { + keyCode: code, + charCode: code, + bubbles: true + }); + gFindBar._findField.dispatchEvent(ev); + } + }); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug360437_window.xhtml b/toolkit/content/tests/chrome/bug360437_window.xhtml new file mode 100644 index 0000000000..dd5e336555 --- /dev/null +++ b/toolkit/content/tests/chrome/bug360437_window.xhtml @@ -0,0 +1,129 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="360437Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + width="600" + height="600" + onload="startTest();" + title="360437 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + const {ContentTask} = ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" + ); + ContentTask.setTestScope(window.arguments[0]); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + gFindBar.browser = gBrowser; + + let loadedPromise = BrowserTestUtils.browserLoaded(gBrowser); + let contentLoadedPromise = ContentTask.spawn(gBrowser, null, async function() { + return new Promise(resolve => { + addEventListener("DOMContentLoaded", () => resolve(), { once: true }); + }); + }); + BrowserTestUtils.startLoadingURIString(gBrowser, "data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>"); + await loadedPromise; + await contentLoadedPromise; + + gFindBar.onFindCommand(); + + // Make sure the findfield is correctly focused on open + var searchStr = "text inside an input element"; + await promiseEnterStringIntoFindField(searchStr); + is(document.commandDispatcher.focusedElement, + gFindBar._findField, "Find field isn't focused"); + + // Make sure "find again" correctly transfers focus to the content element + // when the find bar is closed. + await new Promise(resolve => { + window.addEventListener("findbarclose", resolve, { once: true }); + gFindBar.close(); + }); + gFindBar.onFindAgainCommand(false); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.document.activeElement, + content.document.getElementById("input"), "Input Element isn't focused"); + }); + + // Make sure "find again" doesn't focus the content element if focus + // isn't in the content document. + var textbox = document.getElementById("textbox"); + textbox.focus(); + + ok(gFindBar.hidden, "Findbar is hidden"); + gFindBar.onFindAgainCommand(false); + is(document.activeElement, textbox, + "Focus was stolen from a chrome element"); + } + + function promiseFindResult(str = null) { + return new Promise(resolve => { + let listener = { + onFindResult({ searchString }) { + if (str !== null && str != searchString) { + return; + } + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseEnterStringIntoFindField(str) { + let promise = promiseFindResult(str); + for (let i = 0; i < str.length; i++) { + let event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: str.charCodeAt(i), + }); + gFindBar._findField.dispatchEvent(event); + } + return promise; + } + ]]></script> + <html:input id="textbox"/> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug366992_window.xhtml b/toolkit/content/tests/chrome/bug366992_window.xhtml new file mode 100644 index 0000000000..698d26b43a --- /dev/null +++ b/toolkit/content/tests/chrome/bug366992_window.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="366992 test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="onLoad();" + width="600" + height="600" + title="366992 test"> + + <commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" + oncommandupdate="goUpdateGlobalEditMenuItems()"/> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" + oncommandupdate="goUpdateUndoEditMenuItems()"/> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" + oncommandupdate="goUpdatePasteMenuItems()"/> + <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')"/> + <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')"/> + <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')"/> + <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')"/> + <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')"/> + <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')"/> + <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/> + <command id="cmd_switchTextDirection" oncommand="goDoCommand('cmd_switchTextDirection');"/> + </commandset> + + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/editMenuOverlay.js"/> + <script type="application/javascript"><![CDATA[ + // Without the fix for bug 366992, the delete command would be enabled + // for the input even though the input's controller for this command + // disables it. + var gShouldNotBeReachedController = { + supportsCommand(aCommand) { + return aCommand == "cmd_delete"; + }, + isCommandEnabled(aCommand) { + return aCommand == "cmd_delete"; + }, + doCommand(aCommand) { } + } + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + function finish() { + window.controllers.removeController(gShouldNotBeReachedController); + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + function onLoad() { + document.getElementById("input").focus(); + var deleteDisabled = document.getElementById("cmd_delete") + .getAttribute("disabled") == "true"; + ok(deleteDisabled, + "cmd_delete should be disabled when the empty input is focused"); + finish(); + } + + window.controllers.appendController(gShouldNotBeReachedController); + ]]></script> + + <html:input id="input"/> +</window> diff --git a/toolkit/content/tests/chrome/bug409624_window.xhtml b/toolkit/content/tests/chrome/bug409624_window.xhtml new file mode 100644 index 0000000000..88033ba794 --- /dev/null +++ b/toolkit/content/tests/chrome/bug409624_window.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="409624test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="409624 test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + + function finish() { + window.close(); + SimpleTest.finish(); + } + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + BrowserTestUtils.startLoadingURIString(gBrowser, 'data:text/html,<h2>Text mozilla</h2><input id="inp" type="text" />'); + } + + function onPageShow() { + gFindBar.clear(); + let textbox = gFindBar.getElement("findbar-textbox"); + + // Clear should work regardless of whether the editor has been lazily + // initialised yet + ok(!gFindBar.hasTransactions, "No transactions when findbar empty"); + textbox.value = "mozilla"; + ok(gFindBar.hasTransactions, "Has transactions when findbar value set without editor init"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call without editor init"); + ok(!gFindBar.hasTransactions, "No transactions after clear() call"); + + gFindBar.open(); + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + ok(!matchCaseCheckbox.checked, "case-insensitivity correctly set"); + + // Simulate typical input + textbox.focus(); + gFindBar.clear(); + sendChar("m"); + ok(gFindBar.hasTransactions, "Has transactions after input"); + let preSelection = gBrowser.contentWindow.getSelection(); + ok(!preSelection.isCollapsed, "Found item and selected range"); + gFindBar.clear(); + is(textbox.value, '', "findbar input value cleared after clear() call"); + let postSelection = gBrowser.contentWindow.getSelection(); + ok(postSelection.isCollapsed, "item found deselected after clear() call"); + let fp = gFindBar.getElement("find-previous"); + ok(fp.disabled, "find-previous button disabled after clear() call"); + let fn = gFindBar.getElement("find-next"); + ok(fn.disabled, "find-next button disabled after clear() call"); + + // Test status updated after a search for text not in page + textbox.focus(); + sendChar("x"); + gFindBar.clear(); + let ftext = gFindBar.getElement("find-status"); + is(ftext.textContent, "", "status text disabled after clear() call"); + + // Test input empty with undo stack non-empty + textbox.focus(); + sendChar("m"); + sendKey("BACK_SPACE"); + ok(gFindBar.hasTransactions, "Has transactions when undo available"); + gFindBar.clear(); + gFindBar.close(); + + finish(); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug429723_window.xhtml b/toolkit/content/tests/chrome/bug429723_window.xhtml new file mode 100644 index 0000000000..52e743239b --- /dev/null +++ b/toolkit/content/tests/chrome/bug429723_window.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="429723Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="429723 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + var gFindBar = null; + var gBrowser; + + function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); + } + + function finish() { + window.close(); + window.arguments[0].SimpleTest.finish(); + } + + function onLoad() { + var _delayedOnLoad = function() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + BrowserTestUtils.startLoadingURIString(gBrowser, "data:text/html,<h2 id='h2'>mozilla</h2>"); + } + setTimeout(_delayedOnLoad, 1000); + } + + function enterStringIntoFindField(aString) { + for (var i=0; i < aString.length; i++) { + var event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: aString.charCodeAt(i), + }); + gFindBar._findField.dispatchEvent(event); + } + } + + function onPageShow() { + var findField = gFindBar._findField; + document.getElementById("cmd_find").doCommand(); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + // Perform search + var searchStr = "z"; + enterStringIntoFindField(searchStr); + + // Highlight search term + var highlight = gFindBar.getElement("highlight"); + if (!highlight.checked) + highlight.click(); + + // Delete search term + var event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: KeyEvent.DOM_VK_BACK_SPACE, + charCode: 0, + }); + gFindBar._findField.dispatchEvent(event); + + var notRed = !findField.hasAttribute("status") || + (findField.getAttribute("status") != "notfound"); + ok(notRed, "Find Bar textbox is correct colour"); + finish(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug451540_window.xhtml b/toolkit/content/tests/chrome/bug451540_window.xhtml new file mode 100644 index 0000000000..f8b8900ac6 --- /dev/null +++ b/toolkit/content/tests/chrome/bug451540_window.xhtml @@ -0,0 +1,255 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="451540test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + title="451540 test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + const SEARCH_TEXT = "minefield"; + + let gFindBar = null; + let gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + + SimpleTest.requestLongerTimeout(2); + + function startTest() { + gFindBar = document.getElementById("FindToolbar"); + gBrowser = document.getElementById("content"); + gBrowser.addEventListener("pageshow", onPageShow, { once: true }); + let data = `data:text/html,<input id="inp" type="text" /> + <textarea id="tarea"/>`; + BrowserTestUtils.startLoadingURIString(gBrowser, data); + } + + function promiseHighlightFinished() { + return new Promise(resolve => { + let listener = { + onHighlightFinished() { + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + async function resetForNextTest(elementId, aText) { + if (!aText) + aText = SEARCH_TEXT; + + // Turn off highlighting + let highlightButton = gFindBar.getElement("highlight"); + if (highlightButton.checked) { + highlightButton.click(); + } + + // Initialise input + info(`setting element value to ${aText}`); + await SpecialPowers.spawn(gBrowser, [{elementId, aText}], async function(args) { + let {elementId, aText} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + element.value = aText; + element.focus(); + }); + info(`just set element value to ${aText}`); + gFindBar._findField.value = SEARCH_TEXT; + + // Perform search and turn on highlighting + gFindBar._find(); + highlightButton.click(); + await promiseHighlightFinished(); + + // Move caret to start of element + info(`focusing element`); + await SpecialPowers.spawn(gBrowser, [elementId], async function(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + element.focus(); + }); + info(`focused element`); + if (navigator.platform.includes("Mac")) { + await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }, gBrowser); + } else { + await BrowserTestUtils.synthesizeKey("KEY_Home", {}, gBrowser); + } + } + + async function testSelection(elementId, expectedRangeCount, message) { + await SpecialPowers.spawn(gBrowser, [{elementId, expectedRangeCount, message}], async function(args) { + let {elementId, expectedRangeCount, message} = args; + let doc = content.document; + let element = doc.getElementById(elementId); + let controller = element.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, expectedRangeCount, message); + }); + } + + async function testInput(elementId, testTypeText) { + let isEditableElement = await SpecialPowers.spawn(gBrowser, [elementId], async function(elementId) { + let doc = content.document; + let element = doc.getElementById(elementId); + let elementClass = ChromeUtils.getClassName(element); + return elementClass === "HTMLInputElement" || + elementClass === "HTMLTextAreaElement"; + }); + if (!isEditableElement) { + return; + } + + let moveCaretToNextWordBoundary = async (aBrowser) => { + if (!navigator.platform.includes("Mac")) { + return BrowserTestUtils.synthesizeKey("KEY_ArrowRight", { accelKey: true }, aBrowser); + } + // macOS does not have default shortcut key to move caret per word. + return SpecialPowers.spawn(aBrowser, [], async () => { + content.docShell.doCommand("cmd_wordNext"); + }); + }; + + // Initialize the findbar + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + matchCase.doCommand(); + } + + // First check match has been correctly highlighted + await resetForNextTest(elementId); + + await testSelection(elementId, 1, testTypeText + " correctly highlighted match"); + + // Test 2: check highlight removed when text added within the highlight + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on text insertion"); + + // Test 3: check highlighting remains when text added before highlight + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at start"); + + // Test 4: check highlighting remains when text added after highlight + await resetForNextTest(elementId); + for (let x = 0; x < SEARCH_TEXT.length; x++) { + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + } + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text insertion at end"); + + // Test 5: deleting text within the highlight + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on text deletion"); + + // Test 6: deleting text at end of highlight + await resetForNextTest(elementId, SEARCH_TEXT + "A"); + for (let x = 0; x < (SEARCH_TEXT + "A").length; x++) { + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + } + await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at end"); + + // Test 7: deleting text at start of highlight + await resetForNextTest(elementId, "A" + SEARCH_TEXT); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " highlight correctly remained on text deletion at start"); + + // Test 8: deleting selection + await resetForNextTest(elementId); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { accelKey: true }, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed highlight on selection deletion"); + + // Test 9: Multiple matches within one editor (part 1) + // Check second match remains highlighted after inserting text into + // first match, and that its highlighting gets removed when the + // second match is edited + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text insertion"); + await moveCaretToNextWordBoundary(gBrowser); + await moveCaretToNextWordBoundary(gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("a", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text insertion"); + + // Test 10: Multiple matches within one editor (part 2) + // Check second match remains highlighted after deleting text in + // first match, and that its highlighting gets removed when the + // second match is edited + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await testSelection(elementId, 2, testTypeText + " correctly highlighted both matches"); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only the first highlight on text deletion"); + await moveCaretToNextWordBoundary(gBrowser); + await moveCaretToNextWordBoundary(gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", {}, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_Backspace", {}, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on text deletion"); + + // Test 11: Multiple matches within one editor (part 3) + // Check second match remains highlighted after deleting selection + // in first match, and that second match highlighting gets correctly + // removed when it has a selection deleted from it + await resetForNextTest(elementId, SEARCH_TEXT + " " + SEARCH_TEXT); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowRight", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { accelKey: true }, gBrowser); + await testSelection(elementId, 1, testTypeText + " correctly removed only first highlight on selection deletion"); + await moveCaretToNextWordBoundary(gBrowser); + await moveCaretToNextWordBoundary(gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }, gBrowser); + await BrowserTestUtils.synthesizeKey("x", { accelKey: true }, gBrowser); + await testSelection(elementId, 0, testTypeText + " correctly removed second highlight on selection deletion"); + + // Turn off highlighting + let highlightButton = gFindBar.getElement("highlight"); + if (highlightButton.checked) { + highlightButton.click(); + } + } + + function onPageShow() { + (async function() { + gFindBar.open(); + await testInput("inp", "Input:"); + await testInput("tarea", "Textarea:"); + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForFocus(startTest, window); + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/bug624329_window.xhtml b/toolkit/content/tests/chrome/bug624329_window.xhtml new file mode 100644 index 0000000000..8cef32e4e5 --- /dev/null +++ b/toolkit/content/tests/chrome/bug624329_window.xhtml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Test for bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + context="menu"> + + <script> + window.arguments[0].SimpleTest.waitForFocus(window.arguments[0].childFocused, window); + </script> + + <menupopup id="menu"> + <!-- The bug demonstrated only when the accesskey was presented separately + from the label. + e.g. because the accesskey is not a letter in the label. + + The bug demonstrates only on the first show of the context menu + unless menu items are removed/added each time the menu is + constructed. --> + <menuitem label="Long label to ensure the popup would hit the right of the screen" accesskey="1"/> + </menupopup> +</window> diff --git a/toolkit/content/tests/chrome/chrome.toml b/toolkit/content/tests/chrome/chrome.toml new file mode 100644 index 0000000000..70fa12c4b6 --- /dev/null +++ b/toolkit/content/tests/chrome/chrome.toml @@ -0,0 +1,360 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "../widgets/popup_shared.js", + "../widgets/tree_shared.js", + "RegisterUnregisterChrome.js", + "bug263683_window.xhtml", + "bug304188_window.xhtml", + "bug331215_window.xhtml", + "bug360437_window.xhtml", + "bug366992_window.xhtml", + "bug409624_window.xhtml", + "bug429723_window.xhtml", + "bug624329_window.xhtml", + "dialog_button.xhtml", + "dialog_dialogfocus.xhtml", + "dialog_dialogfocus2.xhtml", + "file_empty.xhtml", + "file_edit_contextmenu.xhtml", + "file_about_networking_wsh.py", + "file_autocomplete_with_composition.js", + "file_editor_with_autocomplete.js", + "findbar_entireword_window.xhtml", + "findbar_events_window.xhtml", + "findbar_window.xhtml", + "frame_popup_anchor.xhtml", + "frame_subframe_origin_subframe1.xhtml", + "frame_subframe_origin_subframe2.xhtml", + "popup_trigger.js", + "sample_entireword_latin1.html", + "window_browser_drop.xhtml", + "window_keys.xhtml", + "window_largemenu.xhtml", + "window_panel.xhtml", + "window_panel_anchoradjust.xhtml", + "window_popup_anchor.xhtml", + "window_popup_anchoratrect.xhtml", + "window_popup_attribute.xhtml", + "window_popup_button.xhtml", + "window_popup_preventdefault_chrome.xhtml", + "window_preferences.xhtml", + "window_preferences2.xhtml", + "window_preferences3.xhtml", + "window_preferences_commandretarget.xhtml", + "window_preferences_disabled.xhtml", + "window_screenPosSize.xhtml", + "window_showcaret.xhtml", + "window_subframe_origin.xhtml", + "window_tooltip.xhtml", + "xul_selectcontrol.js", +] +prefs = [ + "gfx.font_rendering.fallback.async=false", + "widget.non-native-theme.enabled=false", +] + +["test_about_networking.html"] + +["test_arrowpanel.xhtml"] +skip-if = [ + "os == 'win' && verify", + "win10_2009", # Bug 1727507 + "win11_2009", # Bug 1797751 +] + +["test_autocomplete2.xhtml"] + +["test_autocomplete3.xhtml"] + +["test_autocomplete4.xhtml"] + +["test_autocomplete5.xhtml"] + +["test_autocomplete_emphasis.xhtml"] + +["test_autocomplete_mac_caret.xhtml"] +run-if = ["os == 'mac'"] + +["test_autocomplete_placehold_last_complete.xhtml"] + +["test_autocomplete_with_composition_on_input.html"] +skip-if = [ + "apple_catalina" # Bug 1784825 +] +["test_browser_drop.xhtml"] + +["test_bug1048178.xhtml"] +skip-if = ["apple_catalina"] + +["test_bug263683.xhtml"] +skip-if = [ + "debug && os == 'linux'", + "debug && os == 'win'", +] + +["test_bug304188.xhtml"] +skip-if = ["true"] + +["test_bug331215.xhtml"] +skip-if = ["true"] # Bug 1339326 #Bug 1582327 + +["test_bug360220.xhtml"] + +["test_bug360437.xhtml"] +skip-if = ["true"] # Bug 1264604 # Bug 1784826 + +["test_bug365773.xhtml"] + +["test_bug366992.xhtml"] + +["test_bug382990.xhtml"] + +["test_bug409624.xhtml"] + +["test_bug418874.xhtml"] + +["test_bug429723.xhtml"] + +["test_bug451540.xhtml"] +support-files = ["bug451540_window.xhtml"] + +["test_bug457632.xhtml"] + +["test_bug460942.xhtml"] + +["test_bug471776.xhtml"] + +["test_bug509732.xhtml"] + +["test_bug557987.xhtml"] + +["test_bug562554.xhtml"] + +["test_bug624329.xhtml"] +fail-if = ["os == 'linux' && os_version == '18.04'"] # Bug 1600194 + +["test_bug792324.xhtml"] + +["test_button.xhtml"] + +["test_chromemargin.xhtml"] +support-files = "window_chromemargin.xhtml" +skip-if = ["apple_catalina"] + +["test_closemenu_attribute.xhtml"] + +["test_contextmenu_list.xhtml"] + +["test_contextmenu_rtl.xhtml"] + +["test_cursorsnap.xhtml"] +disabled = true +#skip-if = os != 'win' +support-files = [ + "window_cursorsnap_dialog.xhtml", + "window_cursorsnap_wizard.xhtml", +] + +["test_custom_element_base.xhtml"] + +["test_custom_element_delay_connection.xhtml"] + +["test_custom_element_parts.html"] + +["test_deck.xhtml"] + +["test_dialog_button.xhtml"] + +["test_dialogfocus.xhtml"] + +["test_edit_contextmenu.html"] + +["test_editor_for_input_with_autocomplete.html"] + +["test_findbar.xhtml"] +skip-if = ["apple_catalina"] # macosx1014/15 due to 1550078 + +["test_findbar_entireword.xhtml"] + +["test_findbar_events.xhtml"] + +["test_frames.xhtml"] + +["test_hiddenitems.xhtml"] + +["test_hiddenpaging.xhtml"] + +["test_keys.xhtml"] + +["test_labelcontrol.xhtml"] + +["test_largemenu.html"] +skip-if = ["os == 'linux' && !debug"] # Bug 1207174 + +["test_maximized_persist.xhtml"] +support-files = [ + "window_maximized_persist.xhtml", + "file_maximized_persist.js", +] + +["test_maximized_persist_with_no_titlebar.xhtml"] +support-files = [ + "window_maximized_persist_with_no_titlebar.xhtml", + "file_maximized_persist.js", +] + +["test_menu.xhtml"] + +["test_menu_activateitem.xhtml"] + +["test_menu_hide.xhtml"] + +["test_menu_mouse_menuactive.xhtml"] + +["test_menu_withcapture.xhtml"] + +["test_menuchecks.xhtml"] + +["test_menuitem_blink.xhtml"] + +["test_menuitem_commands.xhtml"] + +["test_menulist.xhtml"] + +["test_menulist_in_popup.xhtml"] + +["test_menulist_keynav.xhtml"] + +["test_menulist_null_value.xhtml"] + +["test_menulist_paging.xhtml"] + +["test_menulist_position.xhtml"] + +["test_mousescroll.xhtml"] + +["test_mozinputbox_dictionary.xhtml"] + +["test_named_deck.html"] + +["test_navigate_persist.html"] +support-files = ["window_navigate_persist.html"] + +["test_notificationbox.xhtml"] +skip-if = [ + "os == 'linux' && debug", # Bug 1429649 + "os == 'win'", # Bug 1429649 +] + +["test_panel.xhtml"] +skip-if = ["apple_catalina"] # macosx1014 due to 1550078 + +["test_panel_anchoradjust.xhtml"] + +# test_panel_focus.xhtml won't work if the Full Keyboard Access preference is set to +# textboxes and lists only, so skip this test on Mac +["test_panel_focus.xhtml"] +support-files = "window_panel_focus.xhtml" +skip-if = ["apple_catalina"] + +["test_panel_hover_menu.xhtml"] + +["test_panel_open.xhtml"] + +["test_panelfrommenu.xhtml"] + +["test_popup_anchor.xhtml"] + +["test_popup_anchoratrect.xhtml"] +skip-if = ["os == 'linux'"] # 1167694 + +["test_popup_attribute.xhtml"] +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # Bug 1582610 + "apple_catalina && debug", # Bug 1281360 +] + +["test_popup_button.xhtml"] +skip-if = [ + "os == 'linux' && os_version == '18.04' && asan", # Bug 1582610 + "apple_catalina && debug", # Bug 1281360 +] + +["test_popup_coords.xhtml"] + +["test_popup_keys.xhtml"] + +["test_popup_moveToAnchor.xhtml"] + +["test_popup_preventdefault.xhtml"] + +["test_popup_preventdefault_chrome.xhtml"] + +["test_popup_recreate.xhtml"] + +["test_popup_scaled.xhtml"] + +["test_popup_tree.xhtml"] + +["test_popuphidden.xhtml"] + +["test_popupincontent.xhtml"] +skip-if = ["verify && os == 'win'"] + +["test_popupremoving.xhtml"] + +["test_position.xhtml"] + +["test_preferences.xhtml"] + +["test_preferences_beforeaccept.xhtml"] +support-files = ["window_preferences_beforeaccept.xhtml"] + +["test_preferences_onsyncfrompreference.xhtml"] +support-files = ["window_preferences_onsyncfrompreference.xhtml"] + +["test_props.xhtml"] + +["test_radio.xhtml"] + +["test_richlistbox.xhtml"] + +["test_screenPersistence.xhtml"] + +["test_scrollbar.xhtml"] + +["test_showcaret.xhtml"] + +["test_subframe_origin.xhtml"] + +["test_tabbox.xhtml"] + +["test_tabindex.xhtml"] + +["test_textbox_search.xhtml"] + +["test_tooltip.xhtml"] +skip-if = [ + "apple_catalina", # Bug 1141245, frequent timeouts on OSX 10.14, Windows + "os == 'win'", # Bug 1141245, frequent timeouts on OSX 10.14, Windows +] + +["test_tooltip_noautohide.xhtml"] + +["test_tree.xhtml"] + +["test_tree_hier.xhtml"] + +["test_tree_scroll.xhtml"] +support-files = [ + "!/gfx/layers/apz/test/mochitest/apz_test_utils.js", + "!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", +] + +["test_tree_single.xhtml"] + +["test_tree_view.xhtml"] + +["test_window_intrinsic_size.xhtml"] +support-files = ["window_intrinsic_size.xhtml"] diff --git a/toolkit/content/tests/chrome/dialog_button.xhtml b/toolkit/content/tests/chrome/dialog_button.xhtml new file mode 100644 index 0000000000..64bf916fff --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_button.xhtml @@ -0,0 +1,9 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window id='root' xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <dialog id="dialog-focus" + buttons="accept" + buttonlabelaccept="accept" + buttonaccesskeyaccept="a"> + <button id="button"></button> + </dialog> +</window> diff --git a/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml b/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml new file mode 100644 index 0000000000..bcd303b52f --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_dialogfocus.xhtml @@ -0,0 +1,62 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<dialog id="dialog-focus" + buttons="extra2,accept,cancel"> + +<tabbox id="tabbox" hidden="true"> + <tabs> + <tab id="tab" label="Tab"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tabbutton" label="Tab Button"/> + <button id="tabbutton2" label="Tab Button 2"/> + </tabpanel> + </tabpanels> +</tabbox> + +<html:input id="textbox-yes" value="textbox-yes" hidden="true"/> +<html:input id="textbox-no" value="textbox-no" noinitialfocus="true" hidden="true"/> +<button id="one" label="One"/> +<button id="two" label="Two" hidden="true"/> + +<script> +if (window.arguments) { + var step = window.arguments[0]; + switch (step) { + case 2: + document.getElementById("one").setAttribute("noinitialfocus", "true"); + break; + case 3: + document.getElementById("one").hidden = true; + // no-fallthrough + case 4: + document.getElementById("tabbutton2").setAttribute("noinitialfocus", "true"); + // no-fallthrough + case 5: + document.getElementById("tabbutton").setAttribute("noinitialfocus", "true"); + // no-fallthrough + case 6: + document.getElementById("tabbox").hidden = false; + break; + case 7: + window.addEventListener("load", function() { + var two = document.getElementById("two"); + two.hidden = false; + two.focus(); + }); + break; + case 8: + document.getElementById("textbox-yes").hidden = false; + break; + case 9: + document.getElementById("textbox-no").hidden = false; + break; + } +} +</script> + +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml b/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml new file mode 100644 index 0000000000..da4b3dbdfa --- /dev/null +++ b/toolkit/content/tests/chrome/dialog_dialogfocus2.xhtml @@ -0,0 +1,8 @@ +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="root" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog id="dialog-focus" + buttons="none"> + <button id="nonbutton" noinitialfocus="true"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/file_about_networking_wsh.py b/toolkit/content/tests/chrome/file_about_networking_wsh.py new file mode 100644 index 0000000000..57bd353c21 --- /dev/null +++ b/toolkit/content/tests/chrome/file_about_networking_wsh.py @@ -0,0 +1,10 @@ +from mod_pywebsocket import msgutil + + +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + while not request.client_terminated: + msgutil.receive_message(request) diff --git a/toolkit/content/tests/chrome/file_autocomplete_with_composition.js b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js new file mode 100644 index 0000000000..86619a95d1 --- /dev/null +++ b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js @@ -0,0 +1,714 @@ +// nsDoTestsForAutoCompleteWithComposition tests autocomplete with composition. +// Users must include SimpleTest.js and EventUtils.js. + +function waitForCondition(condition, nextTest) { + var tries = 0; + var interval = setInterval(function () { + if (condition() || tries >= 30) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function () { + clearInterval(interval); + nextTest(); + }; +} + +function nsDoTestsForAutoCompleteWithComposition( + aDescription, + aWindow, + aTarget, + aAutoCompleteController, + aIsFunc, + aGetTargetValueFunc, + aOnFinishFunc +) { + this._description = aDescription; + this._window = aWindow; + this._target = aTarget; + this._controller = aAutoCompleteController; + + this._is = aIsFunc; + this._getTargetValue = aGetTargetValueFunc; + this._onFinish = aOnFinishFunc; + + this._target.focus(); + + this._DefaultCompleteDefaultIndex = + this._controller.input.completeDefaultIndex; + + this._doTests(); +} + +nsDoTestsForAutoCompleteWithComposition.prototype = { + _window: null, + _target: null, + _controller: null, + _DefaultCompleteDefaultIndex: false, + _description: "", + + _is: null, + _getTargetValue() { + return "not initialized"; + }, + _onFinish: null, + + _doTests() { + if (++this._testingIndex == this._tests.length) { + this._controller.input.completeDefaultIndex = + this._DefaultCompleteDefaultIndex; + this._onFinish(); + return; + } + + var test = this._tests[this._testingIndex]; + if ( + this._controller.input.completeDefaultIndex != test.completeDefaultIndex + ) { + this._controller.input.completeDefaultIndex = test.completeDefaultIndex; + } + test.execute(this._window); + + if (test.popup) { + waitForCondition( + () => this._controller.input.popupOpen, + this._checkResult.bind(this) + ); + } else { + waitForCondition(() => { + this._controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH; + }, this._checkResult.bind(this)); + } + }, + + _checkResult() { + var test = this._tests[this._testingIndex]; + this._is( + this._getTargetValue(), + test.value, + this._description + ", " + test.description + ": value" + ); + this._is( + this._controller.searchString, + test.searchString, + this._description + ", " + test.description + ": searchString" + ); + this._is( + this._controller.input.popupOpen, + test.popup, + this._description + ", " + test.description + ": popupOpen" + ); + this._doTests(); + }, + + _testingIndex: -1, + _tests: [ + // Simple composition when popup hasn't been shown. + // The autocomplete popup should not be shown during composition, but + // after compositionend, the popup should be shown. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: "compositionend should open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // If composition starts when popup is shown, the compositionstart event + // should cause closing the popup. + { + description: "compositionstart should close the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mo", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mo", + }, + { + description: + "compositionend should research the result and open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mozi", + searchString: "Mozi", + }, + // If composition is cancelled, the value shouldn't be changed. + { + description: "compositionstart should reclose the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "l", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "l" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozil", + searchString: "Mozi", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "ll", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "l" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozill", + searchString: "Mozi", + }, + { + description: + "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mozi", + }, + { + description: "cancled compositionend should reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommit", data: "", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mozi", + searchString: "Mozi", + }, + // But if composition replaces some characters and canceled, the search + // string should be the latest value. + { + description: + "compositionstart with selected string should close the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeKey("VK_LEFT", { shiftKey: true }, aWindow); + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mozi", + }, + { + description: "modifying composition string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mozi", + }, + { + description: + "modifying composition string to empty string shouldn't reopen the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "Mozi", + }, + { + description: + "canceled compositionend should search the result with the latest value", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // If all characters are removed, the popup should be closed. + { + description: + "the value becomes empty by backspace, the popup should be closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + // composition which is canceled shouldn't cause opening the popup. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + { + description: + "canceled compositionend shouldn't open the popup if it was closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + // Down key should open the popup even if the editor is empty. + { + description: "DOWN key should open the popup even if the value is empty", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + }, + popup: true, + value: "", + searchString: "", + }, + // If popup is open at starting composition, the popup should be reopened + // after composition anyway. + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "", + searchString: "", + }, + { + description: + "canceled compositionend should open the popup if it was opened", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "", + searchString: "", + }, + // Type normally, and hit escape, the popup should be closed. + { + description: "ESCAPE should close the popup after typing something", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("M", {}, aWindow); + synthesizeKey("o", {}, aWindow); + synthesizeKey("KEY_Escape", {}, aWindow); + }, + popup: false, + value: "Mo", + searchString: "Mo", + }, + // Even if the popup is closed, composition which is canceled should open + // the popup if the value isn't empty. + // XXX This might not be good behavior, but anyway, this is minor issue... + { + description: "compositionstart shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "z", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "z" }, + }, + aWindow + ); + }, + popup: false, + value: "Moz", + searchString: "Mo", + }, + { + description: "modifying composition string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "zi", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "i" }, + }, + aWindow + ); + }, + popup: false, + value: "Mozi", + searchString: "Mo", + }, + { + description: + "modifying composition string to empty string shouldn't open the popup", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { string: "", clauses: [{ length: 0, attr: 0 }] }, + caret: { start: 0, length: 0 }, + key: { key: "KEY_Backspace" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "Mo", + }, + { + description: + "canceled compositionend shouldn't open the popup if the popup was closed", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Escape" } }, + aWindow + ); + }, + popup: true, + value: "Mo", + searchString: "Mo", + }, + // House keeping... + { + description: "house keeping for next tests", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + // Testing for nsIAutoCompleteInput.completeDefaultIndex being true. + { + description: + "compositionstart shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "M", + clauses: [{ length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 1, length: 0 }, + key: { key: "M" }, + }, + aWindow + ); + }, + popup: false, + value: "M", + searchString: "", + }, + { + description: + "modifying composition string shouldn't open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeCompositionChange( + { + composition: { + string: "Mo", + clauses: [{ length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }], + }, + caret: { start: 2, length: 0 }, + key: { key: "o" }, + }, + aWindow + ); + }, + popup: false, + value: "Mo", + searchString: "", + }, + { + description: + "compositionend should open the popup (completeDefaultIndex is true)", + completeDefaultIndex: true, + execute(aWindow) { + synthesizeComposition( + { type: "compositioncommitasis", key: { key: "KEY_Enter" } }, + aWindow + ); + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + }, + // House keeping... + { + description: "house keeping for next tests", + completeDefaultIndex: false, + execute(aWindow) { + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + popup: false, + value: "", + searchString: "", + }, + ], +}; diff --git a/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml b/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml new file mode 100644 index 0000000000..e4d1a79040 --- /dev/null +++ b/toolkit/content/tests/chrome/file_edit_contextmenu.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> +<script> +customElements.define("shadow-input", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.appendChild(document.createElement("input")); + } +}); +</script> +<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> +<!-- Copied from toolkit/content/editMenuCommands.inc.xul --> +<script type="application/javascript" src="chrome://global/content/editMenuOverlay.js"/> +<commandset id="editMenuCommands"> + <commandset id="editMenuCommandSetAll" commandupdater="true" events="focus,select" + oncommandupdate="goUpdateGlobalEditMenuItems()"/> + <commandset id="editMenuCommandSetUndo" commandupdater="true" events="undo" + oncommandupdate="goUpdateUndoEditMenuItems()"/> + <commandset id="editMenuCommandSetPaste" commandupdater="true" events="clipboard" + oncommandupdate="goUpdatePasteMenuItems()"/> + <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')"/> + <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')"/> + <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')"/> + <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')"/> + <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')"/> + <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')"/> + <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')"/> + <command id="cmd_switchTextDirection" oncommand="goDoCommand('cmd_switchTextDirection');"/> +</commandset> + +<menupopup id="outer-context-menu"> + <menuseparator id="customizeMailToolbarMenuSeparator"/> + <menuitem id="hello" label="Hello" accesskey="H"/> +</menupopup> + +<hbox context="outer-context-menu"> +<html:textarea /> +<html:input /> +<search-textbox /> +<html:shadow-input /> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/file_editor_with_autocomplete.js b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js new file mode 100644 index 0000000000..78611efc70 --- /dev/null +++ b/toolkit/content/tests/chrome/file_editor_with_autocomplete.js @@ -0,0 +1,627 @@ +// nsDoTestsForEditorWithAutoComplete tests basic functions of editor with autocomplete. +// Users must include SimpleTest.js and EventUtils.js, and register "Mozilla" to the autocomplete for the target. + +async function waitForCondition(condition) { + return new Promise(resolve => { + var tries = 0; + var interval = setInterval(function () { + if (condition() || tries >= 60) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function () { + clearInterval(interval); + resolve(); + }; + }); +} + +function nsDoTestsForEditorWithAutoComplete( + aDescription, + aWindow, + aTarget, + aAutoCompleteController, + aIsFunc, + aTodoIsFunc, + aGetTargetValueFunc +) { + this._description = aDescription; + this._window = aWindow; + this._target = aTarget; + this._controller = aAutoCompleteController; + + this._is = aIsFunc; + this._todo_is = aTodoIsFunc; + this._getTargetValue = aGetTargetValueFunc; + + this._target.focus(); + + this._DefaultCompleteDefaultIndex = + this._controller.input.completeDefaultIndex; +} + +nsDoTestsForEditorWithAutoComplete.prototype = { + _window: null, + _target: null, + _controller: null, + _DefaultCompleteDefaultIndex: false, + _description: "", + + _is: null, + _getTargetValue() { + return "not initialized"; + }, + + run: async function runTestsImpl() { + for (let test of this._tests) { + if ( + this._controller.input.completeDefaultIndex != test.completeDefaultIndex + ) { + this._controller.input.completeDefaultIndex = test.completeDefaultIndex; + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + this._target.addEventListener("beforeinput", onBeforeInput); + this._target.addEventListener("input", onInput); + + if (test.execute(this._window, this._target) === false) { + this._target.removeEventListener("beforeinput", onBeforeInput); + this._target.removeEventListener("input", onInput); + continue; + } + + await waitForCondition(() => { + return ( + this._controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH + ); + }); + this._target.removeEventListener("beforeinput", onBeforeInput); + this._target.removeEventListener("input", onInput); + this._checkResult(test, beforeInputEvents, inputEvents); + } + this._controller.input.completeDefaultIndex = + this._DefaultCompleteDefaultIndex; + }, + + _checkResult(aTest, aBeforeInputEvents, aInputEvents) { + this._is( + this._getTargetValue(), + aTest.value, + this._description + ", " + aTest.description + ": value" + ); + this._is( + this._controller.searchString, + aTest.searchString, + this._description + ", " + aTest.description + ": searchString" + ); + this._is( + this._controller.input.popupOpen, + aTest.popup, + this._description + ", " + aTest.description + ": popupOpen" + ); + this._is( + this._controller.searchStatus, + Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH, + this._description + ", " + aTest.description + ": status" + ); + this._is( + aBeforeInputEvents.length, + aTest.inputEvents.length, + this._description + + ", " + + aTest.description + + ": number of beforeinput events wrong" + ); + this._is( + aInputEvents.length, + aTest.inputEvents.length, + this._description + + ", " + + aTest.description + + ": number of input events wrong" + ); + for (let events of [aBeforeInputEvents, aInputEvents]) { + for (let i = 0; i < events.length; i++) { + if (aTest.inputEvents[i] === undefined) { + this._is( + true, + false, + this._description + + ", " + + aTest.description + + ': "beforeinput" and "input" event shouldn\'t be dispatched anymore' + ); + return; + } + this._is( + events[i] instanceof this._window.InputEvent, + true, + `${this._description}, ${aTest.description}: "${events[i].type}" event should be dispatched with InputEvent interface` + ); + let expectCancelable = + events[i].type === "beforeinput" && + (aTest.inputEvents[i].inputType !== "insertReplacementText" || + SpecialPowers.getBoolPref( + "dom.input_event.allow_to_cancel_set_user_input" + )); + + this._is( + events[i].cancelable, + expectCancelable, + `${this._description}, ${aTest.description}: "${ + events[i].type + }" event should ${expectCancelable ? "be" : "be never"} cancelable` + ); + this._is( + events[i].bubbles, + true, + `${this._description}, ${aTest.description}: "${events[i].type}" event should always bubble` + ); + this._is( + events[i].inputType, + aTest.inputEvents[i].inputType, + `${this._description}, ${aTest.description}: inputType of "${events[i].type}" event should be "${aTest.inputEvents[i].inputType}"` + ); + this._is( + events[i].data, + aTest.inputEvents[i].data, + `${this._description}, ${aTest.description}: data of "${events[i].type}" event should be ${aTest.inputEvents[i].data}` + ); + this._is( + events[i].dataTransfer, + null, + `${this._description}, ${aTest.description}: dataTransfer of "${events[i].type}" event should be null` + ); + this._is( + events[i].getTargetRanges().length, + 0, + `${this._description}, ${aTest.description}: getTargetRanges() of "${events[i].type}" event should return empty array` + ); + } + } + }, + + _tests: [ + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: type 'Mo'", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("M", { shiftKey: true }, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [ + { inputType: "insertText", data: "M" }, + { inputType: "insertText", data: "o" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: select 'Mozilla' to complete the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: undo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: redo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: redo the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case: removing all text for next test...", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + { + description: + "Undo/Redo behavior check when typed text does not match the case: type 'mo'", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("m", {}, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [ + { inputType: "insertText", data: "m" }, + { inputType: "insertText", data: "o" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: select 'Mozilla' to complete the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: undo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: redo the typed text", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: redo the word", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "historyRedo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case: removing all text for next test...", + completeDefaultIndex: false, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + // Testing for nsIAutoCompleteInput.completeDefaultIndex being true. + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): type 'Mo'", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("M", { shiftKey: true }, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [ + { inputType: "insertText", data: "M" }, + { inputType: "insertText", data: "o" }, + { inputType: "insertReplacementText", data: "Mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mo", + searchString: "Mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): undo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [ + { inputType: "historyRedo", data: null }, + { inputType: "insertReplacementText", data: "Mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): redo the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "Mozilla", + searchString: "Mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text exactly matches the case (completeDefaultIndex is true): removing all text for next test...", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): type 'mo'", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("m", {}, aWindow); + synthesizeKey("o", {}, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [ + { inputType: "insertText", data: "m" }, + { inputType: "insertText", data: "o" }, + { inputType: "insertReplacementText", data: "mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): select 'Mozilla' to complete the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("KEY_ArrowDown", {}, aWindow); + synthesizeKey("KEY_Enter", {}, aWindow); + return true; + }, + popup: false, + value: "Mozilla", + searchString: "Mozilla", + inputEvents: [{ inputType: "insertReplacementText", data: "Mozilla" }], + }, + // Different from "exactly matches the case" case, modifying the case causes one additional transaction. + // Although we could make this transaction ignored. + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the selected word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mozilla", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the word, but typed text shouldn't be canceled", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: true, + value: "mo", + searchString: "mo", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): undo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("z", { accelKey: true }, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "historyUndo", data: null }], + }, + // XXX This is odd case. Consistency with undo behavior, this should restore "mo". + // However, looks like that autocomplete automatically restores "mozilla". + // Additionally, looks like that it causes clearing the redo stack. + // Therefore, the following redo operations do nothing. + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the typed text", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [ + { inputType: "historyRedo", data: null }, + { inputType: "insertReplacementText", data: "mozilla" }, + ], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the default index word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): redo the word", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("Z", { accelKey: true, shiftKey: true }, aWindow); + return true; + }, + popup: true, + value: "mozilla", + searchString: "mo", + inputEvents: [], + }, + { + description: + "Undo/Redo behavior check when typed text does not match the case (completeDefaultIndex is true): removing all text for next test...", + completeDefaultIndex: true, + execute(aWindow, aTarget) { + synthesizeKey("a", { accelKey: true }, aWindow); + synthesizeKey("KEY_Backspace", {}, aWindow); + return true; + }, + popup: false, + value: "", + searchString: "", + inputEvents: [{ inputType: "deleteContentBackward", data: null }], + }, + ], +}; diff --git a/toolkit/content/tests/chrome/file_empty.xhtml b/toolkit/content/tests/chrome/file_empty.xhtml new file mode 100644 index 0000000000..cda0d080bd --- /dev/null +++ b/toolkit/content/tests/chrome/file_empty.xhtml @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8"?> +<html xmlns='http://www.w3.org/1999/xhtml'> +</html>
\ No newline at end of file diff --git a/toolkit/content/tests/chrome/file_maximized_persist.js b/toolkit/content/tests/chrome/file_maximized_persist.js new file mode 100644 index 0000000000..c0d543728b --- /dev/null +++ b/toolkit/content/tests/chrome/file_maximized_persist.js @@ -0,0 +1,141 @@ +SimpleTest.waitForExplicitFinish(); +const WIDTH = 300; +const HEIGHT = 300; +let gWindow; +let gTitlebar; + +function promiseMessage(msg) { + info(`wait for message "${msg}"`); + return new Promise(resolve => { + function listener(evt) { + info(`got message "${evt.data}"`); + if (evt.data == msg) { + window.removeEventListener("message", listener); + resolve(); + } + } + window.addEventListener("message", listener); + }); +} + +function openWindow(features = "") { + return window.browsingContext.topChromeWindow.openDialog( + gWindow, + "_blank", + "chrome,dialog=no,all," + features, + window + ); +} + +function checkWindow(msg, win, sizemode, width, height) { + is(win.windowState, sizemode, "sizemode should match " + msg); + if (sizemode == win.STATE_NORMAL) { + is(win.innerWidth, width, "width should match " + msg); + is(win.innerHeight, height, "height should match " + msg); + } +} + +function todoCheckWindow(msg, win, sizemode) { + todo_is(win.windowState, sizemode, "sizemode should match " + msg); +} + +// Persistence of "sizemode" is delayed to 500ms after it's changed. +// See SIZE_PERSISTENCE_TIMEOUT in nsWebShellWindow.cpp. +// We wait for 1000ms to ensure that it is actually persisted. +// We can also wait for condition that XULStore does have the value +// set, but that way we cannot test the cases where we don't expect +// persistence to happen. +function waitForSizeModePersisted() { + return new Promise(resolve => { + setTimeout(resolve, 1000); + }); +} + +async function changeSizeMode(func) { + let promiseSizeModeChange = promiseMessage("sizemodechange"); + func(); + await promiseSizeModeChange; + await waitForSizeModePersisted(); +} + +async function runTest(aWindow) { + gWindow = aWindow; + gTitlebar = aWindow != "window_maximized_persist_with_no_titlebar.xhtml"; + let win = openWindow(); + await SimpleTest.promiseFocus(win); + + // Check the default state. + const chrome_url = win.location.href; + checkWindow("when open initially", win, win.STATE_NORMAL, WIDTH, HEIGHT); + const widthDiff = win.outerWidth - win.innerWidth; + const heightDiff = win.outerHeight - win.innerHeight; + // Maximize the window. + await changeSizeMode(() => win.maximize()); + checkWindow("after maximize window", win, win.STATE_MAXIMIZED); + win.close(); + + // Open a new window to check persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen to maximized", win, win.STATE_MAXIMIZED); + // Restore the window. + if (win.windowState == win.STATE_MAXIMIZED) { + await changeSizeMode(() => win.restore()); + } + checkWindow("after restore window", win, win.STATE_NORMAL, WIDTH, HEIGHT); + win.close(); + + // Open a new window again to check persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen to normal", win, win.STATE_NORMAL, WIDTH, HEIGHT); + // And maximize the window again for next test. + await changeSizeMode(() => win.maximize()); + win.close(); + + // Open a new window again with centerscreen which shouldn't revert + // the persisted sizemode. + win = openWindow("centerscreen"); + await SimpleTest.promiseFocus(win); + checkWindow("when open with centerscreen", win, win.STATE_MAXIMIZED); + win.close(); + + // Linux doesn't seem to persist sizemode across opening window + // with specified size, so mark it expected fail for now. + const isLinux = navigator.platform.includes("Linux"); + let checkWindowMayFail = isLinux ? todoCheckWindow : checkWindow; + + // Open a new window with size specified. + win = openWindow("width=400,height=400"); + await SimpleTest.promiseFocus(win); + checkWindow("when reopen with size", win, win.STATE_NORMAL, 400, 400); + await waitForSizeModePersisted(); + win.close(); + + // Open a new window without size specified. + // The window opened before should not change persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowMayFail("when reopen without size", win, win.STATE_MAXIMIZED); + win.close(); + + // Open a new window with sizing synchronously. + win = openWindow(); + win.resizeTo(500 + widthDiff, 500 + heightDiff); + await SimpleTest.promiseFocus(win); + checkWindow("when sized synchronously", win, win.STATE_NORMAL, 500, 500); + await waitForSizeModePersisted(); + win.close(); + + // Open a new window without any sizing. + // The window opened before should not change persisted sizemode. + win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowMayFail("when reopen without sizing", win, win.STATE_MAXIMIZED); + win.close(); + + // Clean up the XUL store for the given window. + Services.xulStore.removeDocument(chrome_url); + + SimpleTest.finish(); +} diff --git a/toolkit/content/tests/chrome/findbar_entireword_window.xhtml b/toolkit/content/tests/chrome/findbar_entireword_window.xhtml new file mode 100644 index 0000000000..1cb85cffca --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_entireword_window.xhtml @@ -0,0 +1,274 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window id="FindbarEntireWordTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test - entire words only"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + var gFindBar = null; + var gBrowser; + + var SimpleTest = window.arguments[0].SimpleTest; + var SpecialPowers = window.arguments[0].SpecialPowers; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var isnot = window.arguments[0].isnot; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + const kBaseURL = "chrome://mochitests/content/chrome/toolkit/content/tests/chrome"; + const kTests = { + latin1: { + testSimpleEntireWord: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testCaseSensitive: { + "And": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'And' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 5, "should've found 5 matches"); + }, + "Mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testWordBreakChars: { + "a": results => { + // 'a' is a common charactar, but there should only be one occurrence + // separated by word boundaries. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'a' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + }, + "quarrelled": results => { + // 'quarrelled' is denoted as a word by a period char. + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'quarrelled' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + }, + testQuickfind: { + "and": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'and' should've been found"); + is(results.matches.total, 6, "should've found 6 matches"); + }, + "an": results => { + is(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'an' shouldn't have been found"); + is(results.matches.total, 0, "should've found 0 matches"); + }, + "darkness": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'darkness' should've been found"); + is(results.matches.total, 3, "should've found 3 matches"); + }, + "mammon": results => { + isnot(results.find.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "'mammon' should've been found"); + is(results.matches.total, 1, "should've found 1 match"); + } + } + } + }; + + function onLoad() { + (async function() { + await SpecialPowers.pushPrefEnv( + { set: [["findbar.entireword", true]] }); + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + // XXXmikedeboer: when multiple test samples are available, make this + // a nested loop that iterates over them. For now, only + // latin is available. + await startTestWithBrowser("latin1", browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(testName, browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, kBaseURL + "/sample_entireword_" + testName + ".html"); + await promise; + gFindBar.browser = gBrowser; + await onDocumentLoaded(testName); + } + + async function onDocumentLoaded(testName) { + let suite = kTests[testName]; + await testSimpleEntireWord(suite.testSimpleEntireWord); + await testCaseSensitive(suite.testCaseSensitive); + await testWordBreakChars(suite.testWordBreakChars); + await testQuickfind(suite.testQuickfind); + } + + var enterStringIntoFindField = async function(str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: str.charCodeAt(i), + }); + gFindBar._findField.dispatchEvent(event); + if (waitForResult) { + await promise; + } + } + }; + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + function promiseFindResult(searchString) { + return new Promise(resolve => { + let data = {}; + let listener = { + onFindResult: res => { + if (searchString && res.searchString != searchString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + data.find = res; + if (res.result == Ci.nsITypeAheadFind.FIND_NOTFOUND) { + data.matches = { total: 0, current: 0 }; + resolve(data); + return; + } + listener = { + onMatchesCountResult: res => { + if (searchString && res.searchString != searchString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + data.matches = res; + resolve(data); + } + }; + gFindBar.browser.finder.addResultListener(listener); + } + }; + + gFindBar.browser.finder.addResultListener(listener); + }); + } + + async function testIterator(tests) { + for (let searchString of Object.getOwnPropertyNames(tests)) { + gFindBar.clear(); + + let promise = promiseFindResult(searchString); + + await enterStringIntoFindField(searchString, false); + + let result = await promise; + tests[searchString](result); + } + } + + async function testSimpleEntireWord(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testSimpleEntireWord: findbar should be open"); + + await testIterator(tests); + + gFindBar.close(); + } + + async function testCaseSensitive(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testCaseSensitive: findbar should be open"); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && !matchCaseCheckbox.checked) + matchCaseCheckbox.click(); + + await testIterator(tests); + + if (!matchCaseCheckbox.hidden) + matchCaseCheckbox.click(); + gFindBar.close(); + } + + async function testWordBreakChars(tests) { + await openFindbar(); + ok(!gFindBar.hidden, "testWordBreakChars: findbar should be open"); + + await testIterator(tests); + + gFindBar.close(); + } + + async function testQuickfind(tests) { + await SpecialPowers.spawn(gBrowser, [], async function() { + let event = new content.window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: "/".charCodeAt(0), + }); + content.document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickfind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickfind: find field is not focused"); + ok(!gFindBar.getElement("entire-word-status").hidden, + "testQuickfind: entire word mode status text should be visible"); + + await testIterator(tests); + + gFindBar.close(); + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_events_window.xhtml b/toolkit/content/tests/chrome/findbar_events_window.xhtml new file mode 100644 index 0000000000..bc02ef6bcc --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_events_window.xhtml @@ -0,0 +1,195 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="SimpleTest.executeSoon(startTest);" + title="findbar events test"> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + var gFindBar = null; + var gBrowser; + const kTimeout = 5000; // 5 seconds. + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + function startTest() { + (async function() { + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + const url = "data:text/html,hello there"; + let promise = BrowserTestUtils.browserLoaded(gBrowser, false, url); + BrowserTestUtils.startLoadingURIString(gBrowser, url); + await promise; + gFindBar.browser = gBrowser; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + gFindBar.open(); + gFindBar.onFindCommand(); + + await testFind(); + await testFindAgain(); + await testCaseSensitivity(); + await testDiacriticMatching(); + await testHighlight(); + } + + function checkSelection() { + return new Promise(resolve => { + SimpleTest.executeSoon(() => { + SpecialPowers.spawn(gBrowser, [], async function() { + let selected = content.getSelection(); + Assert.equal(String(selected), "", "No text is selected"); + + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let selection = controller.getSelection(controller.SELECTION_FIND); + Assert.equal(selection.rangeCount, 0, "No text is highlighted"); + }).then(resolve); + }); + }); + } + + function once(node, eventName, preventDefault = true) { + return new Promise((resolve, reject) => { + let timeout = window.setTimeout(() => { + reject("Event wasn't fired within " + kTimeout + "ms for event '" + + eventName + "'."); + }, kTimeout); + + node.addEventListener(eventName, function clb(e) { + window.clearTimeout(timeout); + node.removeEventListener(eventName, clb); + if (preventDefault) + e.preventDefault(); + resolve(e); + }); + }); + } + + async function testFind() { + info("Testing normal find."); + let query = "t"; + let promise = once(gFindBar, "find"); + + // Put some text in the find box. + let event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: query.charCodeAt(0), + }); + gFindBar._findField.dispatchEvent(event); + + let e = await promise; + ok(e.detail.query === query, "find event query should match '" + query + "'"); + // Since we're preventing the default make sure nothing was selected. + await checkSelection(); + } + + async function testFindAgain() { + info("Testing repeating normal find."); + let promise = once(gFindBar, "findagain"); + + gFindBar.onFindAgainCommand(); + + await promise; + // Since we're preventing the default make sure nothing was selected. + await checkSelection(); + } + + async function testCaseSensitivity() { + info("Testing normal case sensitivity."); + let promise = once(gFindBar, "findcasesensitivitychange", false); + + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + matchCaseCheckbox.click(); + + let e = await promise; + ok(e.detail.caseSensitive, "find should be case sensitive"); + + // Toggle it back to the original setting. + matchCaseCheckbox.click(); + + // Changing case sensitivity does the search so clear the selected text + // before the next test. + await SpecialPowers.spawn(gBrowser, [], () => content.getSelection().removeAllRanges()); + } + + async function testDiacriticMatching() { + info("Testing normal diacritic matching."); + let promise = once(gFindBar, "finddiacriticmatchingchange", false); + + let matchDiacriticsCheckbox = gFindBar.getElement("find-match-diacritics"); + matchDiacriticsCheckbox.click(); + + let e = await promise; + ok(e.detail.matchDiacritics, "find should match diacritics"); + + // Toggle it back to the original setting. + matchDiacriticsCheckbox.click(); + + // Changing diacritic matching does the search so clear the selected text + // before the next test. + await SpecialPowers.spawn(gBrowser, [], () => content.getSelection().removeAllRanges()); + } + + async function testHighlight() { + info("Testing find with highlight all."); + // Update the find state so the highlight button is clickable. + gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false); + + let promise = once(gFindBar, "findhighlightallchange"); + + let highlightButton = gFindBar.getElement("highlight"); + if (!highlightButton.checked) + highlightButton.click(); + + let e = await promise; + ok(e.detail.highlightAll, "find event should have highlight all set"); + // Since we're preventing the default make sure nothing was highlighted. + await checkSelection(); + + // Toggle it back to the original setting. + if (highlightButton.checked) + highlightButton.click(); + } + ]]></script> + + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/findbar_window.xhtml b/toolkit/content/tests/chrome/findbar_window.xhtml new file mode 100644 index 0000000000..76bf1cae13 --- /dev/null +++ b/toolkit/content/tests/chrome/findbar_window.xhtml @@ -0,0 +1,792 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window id="FindbarTest" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="600" + height="600" + onload="onLoad();" + title="findbar test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript"><![CDATA[ + const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" + ); + + const SAMPLE_URL = "http://www.mozilla.org/"; + const SAMPLE_TEXT = "Some text in a text field."; + const SEARCH_TEXT = "Text Test (δοκιμή)"; + const NOT_FOUND_TEXT = "This text is not on the page." + const ITERATOR_TIMEOUT = Services.prefs.getIntPref("findbar.iteratorTimeout"); + + var gFindBar = null; + var gBrowser; + + var gHasFindClipboard = Services.clipboard.isClipboardTypeSupported(Services.clipboard.kFindClipboard); + + var gStatusText; + var gXULBrowserWindow = { + QueryInterface: ChromeUtils.generateQI(["nsIXULBrowserWindow"]), + + setOverLink(aStatusText) { + gStatusText = aStatusText; + }, + + onBeforeLinkTraversal() { } + }; + + var SimpleTest = window.arguments[0].SimpleTest; + var ok = window.arguments[0].ok; + var is = window.arguments[0].is; + var info = window.arguments[0].info; + SimpleTest.requestLongerTimeout(2); + + function onLoad() { + (async function() { + window.docShell + .treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIAppWindow) + .XULBrowserWindow = gXULBrowserWindow; + + gFindBar = document.getElementById("FindToolbar"); + for (let browserId of ["content", "content-remote"]) { + await startTestWithBrowser(browserId); + } + })().then(() => { + window.close(); + SimpleTest.finish(); + }); + } + + async function startTestWithBrowser(browserId) { + info("Starting test with browser '" + browserId + "'"); + gBrowser = document.getElementById(browserId); + + // Tests delays the loading of a document for one second. + await new Promise(resolve => setTimeout(resolve, 1000)); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + BrowserTestUtils.startLoadingURIString(gBrowser, "data:text/html;charset=utf-8,<h2 id='h2'>" + SEARCH_TEXT + + "</h2><h2><a href='" + SAMPLE_URL + "'>Link Test</a></h2><input id='text' type='text' value='" + + SAMPLE_TEXT + "'></input><input id='button' type='button'></input><img id='img' width='50' height='50'/>", + { triggeringPrincipal: window.document.nodePrincipal }); + await promise; + gFindBar.browser = gBrowser; + await onDocumentLoaded(); + } + + async function onDocumentLoaded() { + await testNormalFind(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testNormalFind"); + await openFindbar(); + await testNormalFindWithComposition(); + gFindBar.close(); + ok(gFindBar.hidden, "findbar should be hidden after testNormalFindWithComposition"); + await openFindbar(); + await testAutoCaseSensitivityUI(); + await testQuickFindText(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testQuickFindText"); + // TODO: `testFindWithHighlight` tests fastFind integrity, which can not + // be accessed with RemoteFinder. We're skipping it for now. + if (gFindBar._browser.finder._fastFind) { + await testFindWithHighlight(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindWithHighlight"); + } + await testFindbarSelection(); + ok(gFindBar.hidden, "Failed to close findbar after testFindbarSelection"); + // TODO: I don't know how to drop a content element on a chrome input. + if (!gBrowser.hasAttribute("remote")) + testDrop(); + await testQuickFindLink(); + if (gHasFindClipboard) { + await testStatusText(); + } + + if (!AppConstants.DEBUG) { + await testFindCountUI(); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI"); + await testFindCountUI(true); + gFindBar.close(); + ok(gFindBar.hidden, "Failed to close findbar after testFindCountUI - linksOnly"); + } + + await openFindbar(); + await testFindAfterCaseChanged(); + gFindBar.close(); + await openFindbar(); + await testFailedStringReset(); + gFindBar.close(); + await testQuickFindClose(); + // TODO: This doesn't seem to work when the findbar is connected to a + // remote browser element. + if (!gBrowser.hasAttribute("remote")) + await testFindAgainNotFound(); + await testToggleEntireWord(); + } + + async function testFindbarSelection() { + function checkFindbarState(aTestName, aExpSelection) { + ok(!gFindBar.hidden, "testFindbarSelection: failed to open findbar: " + aTestName); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testFindbarSelection: find field is not focused: " + aTestName); + if (!gHasFindClipboard) { + ok(gFindBar._findField.value == aExpSelection, + "Incorrect selection in testFindbarSelection: " + aTestName + + ". Selection: " + gFindBar._findField.value); + } + + // Clear the value, close the findbar. + gFindBar._findField.value = ""; + gFindBar.close(); + } + + // Test normal selected text. + await SpecialPowers.spawn(gBrowser, [], async function() { + let document = content.document; + let cH2 = document.getElementById("h2"); + let cSelection = content.getSelection(); + let cRange = document.createRange(); + cRange.setStart(cH2, 0); + cRange.setEnd(cH2, 1); + cSelection.removeAllRanges(); + cSelection.addRange(cRange); + }); + await openFindbar(); + checkFindbarState("plain text", SEARCH_TEXT); + + // Test editable element with selection. + await SpecialPowers.spawn(gBrowser, [], async function() { + let textInput = content.document.getElementById("text"); + textInput.focus(); + textInput.select(); + }); + await openFindbar(); + checkFindbarState("text input", SAMPLE_TEXT); + + // Test non-editable input element (type="button"). + await SpecialPowers.spawn(gBrowser, [], async function() { + content.document.getElementById("button").focus(); + }); + await openFindbar(); + checkFindbarState("button", ""); + } + + function testDrop() { + gFindBar.open(); + // use an dummy image to start the drag so it doesn't get interrupted by a selection + var img = gBrowser.contentDocument.getElementById("img"); + synthesizeDrop(img, gFindBar._findField, [[ {type: "text/plain", data: "Rabbits" } ]], "copy", window); + is(gFindBar._findField.value, "Rabbits", "drop on findbar"); + gFindBar.close(); + } + + function testQuickFindClose() { + return new Promise(resolve => { + var _isClosedCallback = function() { + ok(gFindBar.hidden, + "_isClosedCallback: Failed to auto-close quick find bar after " + + gFindBar._quickFindTimeoutLength + "ms"); + resolve(); + }; + setTimeout(_isClosedCallback, gFindBar._quickFindTimeoutLength + 100); + }); + } + + function testStatusText() { + return new Promise(resolve => { + var _delayedCheckStatusText = function() { + ok(gStatusText == SAMPLE_URL, "testStatusText: Failed to set status text of found link"); + resolve(); + }; + setTimeout(_delayedCheckStatusText, 100); + }); + } + + function promiseFindResult(expectedString) { + return new Promise(resolve => { + let listener = { + onFindResult(result) { + if (expectedString && result.searchString != expectedString) { + return; + } + + gFindBar.browser.finder.removeResultListener(listener); + resolve(result); + }, + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + function promiseMatchesCountResult(expectedString) { + return new Promise(resolve => { + let listener = { + onMatchesCountResult(result) { + if (expectedString && result.searchString != expectedString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + }, + }; + gFindBar.browser.finder.addResultListener(listener); + // Make sure we resolve _at least_ after five times the find iterator timeout. + setTimeout(resolve, (ITERATOR_TIMEOUT * 5) + 20); + }); + } + + function promiseHighlightFinished(expectedString) { + return new Promise(resolve => { + let listener = { + onHighlightFinished(result) { + if (expectedString && result.searchString != expectedString) + return; + + gFindBar.browser.finder.removeResultListener(listener); + resolve(); + } + }; + gFindBar.browser.finder.addResultListener(listener); + }); + } + + var enterStringIntoFindField = async function(str, waitForResult = true) { + for (let promise, i = 0; i < str.length; i++) { + if (waitForResult) { + promise = promiseFindResult(); + } + let event = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: str.charCodeAt(i), + }); + gFindBar._findField.dispatchEvent(event); + if (waitForResult) { + await promise; + } + } + }; + + function promiseExpectRangeCount(rangeCount) { + return SpecialPowers.spawn(gBrowser, [{ rangeCount }], async function(args) { + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); + Assert.equal(sel.rangeCount, args.rangeCount, + "Expected the correct amount of ranges inside the Find selection"); + }); + } + + // also test match-case + async function testNormalFind() { + document.getElementById("cmd_find").doCommand(); + + ok(!gFindBar.hidden, "testNormalFind: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testNormalFind: find field is not focused"); + + let promise; + let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + } + + var searchStr = "text tes"; + await enterStringIntoFindField(searchStr); + + let sel = await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFind: failed to find '" + args.searchStr + "'"); + return sel; + }); + testClipboardSearchString(sel); + + if (!matchCaseCheckbox.hidden) { + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + enterStringIntoFindField("t"); + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.notEqual(content.getSelection().toString(), args.searchStr, + "testNormalFind: Case-sensitivy is broken '" + args.searchStr + "'"); + }); + promise = promiseFindResult(); + matchCaseCheckbox.click(); + await promise; + } + } + + function openFindbar() { + document.getElementById("cmd_find").doCommand(); + return gFindBar._startFindDeferred && gFindBar._startFindDeferred.promise; + } + + async function testNormalFindWithComposition() { + ok(!gFindBar.hidden, "testNormalFindWithComposition: findbar should be open"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testNormalFindWithComposition: find field should be focused"); + + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var clicked = false; + if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked) { + matchCaseCheckbox.click(); + clicked = true; + } + + gFindBar._findField.focus(); + + var searchStr = "text"; + + synthesizeCompositionChange( + { "composition": + { "string": searchStr, + "clauses": + [ + { "length": searchStr.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": searchStr.length, "length": 0 } + }); + + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.notEqual(content.getSelection().toString().toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text shouldn't be found during composition"); + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + let sel = await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + let sel = content.getSelection().toString(); + Assert.equal(sel.toLowerCase(), args.searchStr, + "testNormalFindWithComposition: text should be found after committing composition"); + return sel; + }); + testClipboardSearchString(sel); + + if (clicked) { + matchCaseCheckbox.click(); + } + } + + async function testAutoCaseSensitivityUI() { + var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive"); + var matchCaseLabel = gFindBar.getElement("match-case-status"); + ok(!matchCaseCheckbox.hidden, "match case box is hidden in manual mode"); + ok(matchCaseLabel.hidden, "match case label is visible in manual mode"); + + await changeCase(2); + + ok(matchCaseCheckbox.hidden, + "match case box is visible in automatic mode"); + ok(!matchCaseLabel.hidden, + "match case label is hidden in automatic mode"); + + await enterStringIntoFindField("a"); + ok(matchCaseLabel.hidden, + "match case label is hidden in automatic mode with lower-case input"); + await enterStringIntoFindField("A"); + ok(!matchCaseLabel.hidden, + "match case label is visible in automatic mode with upper-case input"); + + // bug 365551 + gFindBar.onFindAgainCommand(); + ok(matchCaseCheckbox.hidden && !matchCaseLabel.hidden, + "bug 365551: case sensitivity UI is broken after find-again"); + await changeCase(0); + gFindBar.close(); + } + + async function clearFocus() { + document.commandDispatcher.focusedElement = null; + document.commandDispatcher.focusedWindow = null; + await SpecialPowers.spawn(gBrowser, [], async function() { + content.focus(); + }); + } + + async function testQuickFindLink() { + await clearFocus(); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let event = new content.window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: "'".charCodeAt(0), + }); + content.document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindLink: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickFindLink: find field is not focused"); + + var searchStr = "Link Test"; + await enterStringIntoFindField(searchStr); + await SpecialPowers.spawn(gBrowser, [{ searchStr }], async function(args) { + Assert.equal(content.getSelection().toString(), args.searchStr, + "testQuickFindLink: failed to find sample link"); + }); + testClipboardSearchString(searchStr); + } + + // See bug 963925 for more details on this test. + async function testFindWithHighlight() { + gFindBar._findField.value = ""; + + // For this test, we want to closely control the selection. The easiest + // way to do so is to replace the implementation of + // Finder.getInitialSelection with a no-op and call the findbar's callback + // (onCurrentSelection(..., true)) ourselves with our hand-picked + // selection. + let oldGetInitialSelection = gFindBar.browser.finder.getInitialSelection; + let searchStr; + gFindBar.browser.finder.getInitialSelection = function(){}; + + let findCommand = document.getElementById("cmd_find"); + findCommand.doCommand(); + + gFindBar.onCurrentSelection("", true); + + searchStr = "e"; + await enterStringIntoFindField(searchStr); + + let a = gFindBar._findField.value; + let b = gFindBar._browser.finder._fastFind.searchString; + let c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 1: " + a + ", " + b + ", " + c + "."); + + searchStr = "t"; + findCommand.doCommand(); + + gFindBar.onCurrentSelection(searchStr, true); + gFindBar.browser.finder.getInitialSelection = oldGetInitialSelection; + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 2: " + searchStr + + ", " + a + ", " + b + ", " + c + "."); + + let highlightButton = gFindBar.getElement("highlight"); + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight 3: Highlight All should be checked."); + + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == searchStr && b == c, "testFindWithHighlight 4: " + a + ", " + b + ", " + c + "."); + + gFindBar.onFindAgainCommand(); + a = gFindBar._findField.value; + b = gFindBar._browser.finder._fastFind.searchString; + c = gFindBar._browser.finder.searchString; + ok(a == b && b == c, "testFindWithHighlight 5: " + a + ", " + b + ", " + c + "."); + + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight: Highlight All should be unchecked."); + + // Regression test for bug 1316515. + searchStr = "e"; + gFindBar.clear(); + await enterStringIntoFindField(searchStr); + await promiseExpectRangeCount(0); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight: Highlight All should be checked."); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + synthesizeKey("KEY_Backspace"); + await promiseExpectRangeCount(0); + + // Regression test for bug 1316513. + highlightButton.click(); + ok(!highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be unchecked."); + await enterStringIntoFindField(searchStr); + + highlightButton.click(); + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All should be checked."); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + let promise = BrowserTestUtils.browserLoaded(gBrowser); + gBrowser.reload(); + await promise; + + ok(highlightButton.checked, "testFindWithHighlight - 1316513: Highlight All " + + "should still be checked after a reload."); + synthesizeKey("KEY_Enter"); + await promiseHighlightFinished(searchStr); + await promiseExpectRangeCount(3); + + // Uncheck at test end to not interfere with other test functions that are + // run after this one. + highlightButton.click(); + } + + async function testQuickFindText() { + await clearFocus(); + + await SpecialPowers.spawn(gBrowser, [], async function() { + let event = new content.window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: "/".charCodeAt(0), + }); + content.document.documentElement.dispatchEvent(event); + }); + + ok(!gFindBar.hidden, "testQuickFindText: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testQuickFindText: find field is not focused"); + + await enterStringIntoFindField(SEARCH_TEXT); + await SpecialPowers.spawn(gBrowser, [{ SEARCH_TEXT }], async function(args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "testQuickFindText: failed to find '" + args.SEARCH_TEXT + "'"); + }); + testClipboardSearchString(SEARCH_TEXT); + } + + async function testFindCountUI(linksOnly = false) { + await clearFocus(); + + if (linksOnly) { + await SpecialPowers.spawn(gBrowser, [], async function() { + let event = new content.window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: "'".charCodeAt(0), + }); + content.document.documentElement.dispatchEvent(event); + }); + } else { + document.getElementById("cmd_find").doCommand(); + } + + ok(!gFindBar.hidden, "testFindCountUI: failed to open findbar"); + ok(document.commandDispatcher.focusedElement == gFindBar._findField, + "testFindCountUI: find field is not focused"); + + let promise; + let matchCase = gFindBar.getElement("find-case-sensitive"); + if (matchCase.checked) { + promise = promiseFindResult(); + matchCase.click(); + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + await promise; + } + + let foundMatches = gFindBar._foundMatches; + let tests = [{ + text: "t", + current: linksOnly ? 1 : 5, + total: linksOnly ? 2 : 10, + }, { + text: "te", + current: linksOnly ? 1 : 3, + total: linksOnly ? 1 : 5, + }, { + text: "tes", + current: 1, + total: linksOnly ? 1 : 2, + }, { + text: "texxx", + current: 0, + total: 0 + }]; + + function assertMatches(aTest) { + const matches = JSON.parse(foundMatches.dataset.l10nArgs); + is(matches.current, aTest.current, + `${linksOnly ? "[Links-only] " : ""}Currently highlighted match should be at ${aTest.current} for '${aTest.text}'`); + is(matches.total, aTest.total, + `${linksOnly ? "[Links-only] " : ""}Total amount of matches should be ${aTest.total} for '${aTest.text}'`); + } + + for (let test of tests) { + gFindBar._findField.select(); + gFindBar._findField.focus(); + + let timeout = ITERATOR_TIMEOUT; + if (test.text.length == 1) + timeout *= 4; + else if (test.text.length == 2) + timeout *= 2; + timeout += 20; + await new Promise(resolve => setTimeout(resolve, timeout)); + await enterStringIntoFindField(test.text, false); + await promiseMatchesCountResult(test.text); + if (!test.total) { + ok(!foundMatches.dataset.l10nId, "No message should be shown when 0 matches are expected"); + } else { + assertMatches(test); + for (let i = 1; i < test.total; i++) { + await new Promise(resolve => setTimeout(resolve, timeout)); + gFindBar.onFindAgainCommand(); + await promiseMatchesCountResult(test.text); + // test.current + 1, test.current + 2, ..., test.total, 1, ..., test.current + let current = (test.current + i - 1) % test.total + 1; + assertMatches({ + text: test.text, + current, + total: test.total + }); + } + } + } + } + + // See bug 1051187. + async function testFindAfterCaseChanged() { + // Search to set focus on "Text Test" so that searching for "t" selects first + // (upper case!) "T". + await enterStringIntoFindField(SEARCH_TEXT); + gFindBar.clear(); + + // Case-insensitive should already be the current value. + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 0); + + await enterStringIntoFindField("t"); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "T", "First T should be selected."); + }); + + await changeCase(1); + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "t", "First t should be selected."); + }); + } + + // Make sure that _findFailedString is cleared: + // 1. Do a search that fails with case sensitivity but matches with no case sensitivity. + // 2. Uncheck case sensitivity button to match the string. + async function testFailedStringReset() { + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", 1); + + let promise = promiseFindResult(gBrowser.hasAttribute("remote") ? SEARCH_TEXT.toUpperCase() : ""); + await enterStringIntoFindField(SEARCH_TEXT.toUpperCase(), false); + await promise; + await SpecialPowers.spawn(gBrowser, [], async function() { + Assert.equal(content.getSelection().toString(), "", "Not found."); + }); + + await changeCase(0); + await SpecialPowers.spawn(gBrowser, [{ SEARCH_TEXT }], async function(args) { + Assert.equal(content.getSelection().toString(), args.SEARCH_TEXT, + "Search text should be selected."); + }); + } + + function testClipboardSearchString(aExpected) { + if (!gHasFindClipboard) + return; + + if (!aExpected) + aExpected = ""; + var searchStr = gFindBar.browser.finder.clipboardSearchString; + ok(searchStr.toLowerCase() == aExpected.toLowerCase(), + "testClipboardSearchString: search string not set to '" + aExpected + + "', instead found '" + searchStr + "'"); + } + + // See bug 967982. + async function testFindAgainNotFound() { + await openFindbar(); + await enterStringIntoFindField(NOT_FOUND_TEXT, false); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + let promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + await promise; + ok(!gFindBar.hidden, "Unsuccessful Find Again opens the find bar."); + + await enterStringIntoFindField(SEARCH_TEXT); + gFindBar.close(); + ok(gFindBar.hidden, "The findbar is closed."); + promise = promiseFindResult(); + gFindBar.onFindAgainCommand(); + await promise; + ok(gFindBar.hidden, "Successful Find Again leaves the find bar closed."); + } + + async function testToggleDiacriticMatching() { + await openFindbar(); + let promise = promiseFindResult(); + await enterStringIntoFindField("δοκιμη", false); + let result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); + + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + promise = promiseFindResult(); + let check = gFindBar.getElement("find-match-diacritics"); + check.click(); + result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); + + check.click(); + gFindBar.close(true); + } + + async function testToggleEntireWord() { + await openFindbar(); + let promise = promiseFindResult(); + await enterStringIntoFindField("Tex", false); + let result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_FOUND, "Text should be found"); + + await new Promise(resolve => setTimeout(resolve, ITERATOR_TIMEOUT + 20)); + promise = promiseFindResult(); + let check = gFindBar.getElement("find-entire-word"); + check.click(); + result = await promise; + is(result.result, Ci.nsITypeAheadFind.FIND_NOTFOUND, "Text should NOT be found"); + + check.click(); + gFindBar.close(true); + } + + function changeCase(value) { + let promise = gBrowser.hasAttribute("remote") ? promiseFindResult() : Promise.resolve(); + Services.prefs.setIntPref("accessibility.typeaheadfind.casesensitive", value); + return promise; + } + ]]></script> + + <commandset> + <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/> + </commandset> + <browser type="content" primary="true" flex="1" id="content" messagemanagergroup="test" src="about:blank"/> + <browser type="content" primary="true" flex="1" id="content-remote" remote="true" messagemanagergroup="test" src="about:blank"/> + <findbar id="FindToolbar" browserid="content"/> +</window> diff --git a/toolkit/content/tests/chrome/frame_popup_anchor.xhtml b/toolkit/content/tests/chrome/frame_popup_anchor.xhtml new file mode 100644 index 0000000000..934cea6faf --- /dev/null +++ b/toolkit/content/tests/chrome/frame_popup_anchor.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<menupopup id="popup" onpopupshowing="if (isSecondTest) popupShowing(event)" onpopupshown="popupShown()" + onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<button id="button" label="OK" popup="popup"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var isSecondTest = false; + +function openPopup() +{ + document.getElementById("popup").openPopup(parent.document.getElementById("outerbutton"), "after_start", 3, 1); +} + +function popupShowing(event) +{ + var buttonrect = document.getElementById("button").getBoundingClientRect(); + parent.arguments[0].SimpleTest.is(event.clientX, buttonrect.left + 6, "popup clientX with mouse"); + parent.arguments[0].SimpleTest.is(event.clientY, buttonrect.top + 6, "popup clientY with mouse"); +} + +function popupShown() +{ + var left, top; + var popup = document.getElementById("popup"); + var popuprect = popup.getBoundingClientRect(); + if (isSecondTest) { + let buttonrect = document.getElementById("button").getBoundingClientRect(); + left = buttonrect.left + 6; + top = buttonrect.top + 6; + } else { + let iframerect = parent.document.getElementById("frame").getBoundingClientRect(); + let buttonrect = parent.document.getElementById("outerbutton").getBoundingClientRect(); + + // The popup should appear anchored on the bottom left edge of the button, however + // the client rectangle is relative to the iframe's document. Thus the coordinates + // are: + // left = iframe's left - anchor button's left - 3 pixel offset passed to openPopup + + // iframe border (17px) + iframe padding (0) + // top = iframe's top - anchor button's bottom - 1 pixel offset passed to openPopup + + // iframe border (0) + iframe padding (3px); + left = -(Math.round(iframerect.left) - Math.round(buttonrect.left) + 14); + top = -(Math.round(iframerect.top) - Math.round(buttonrect.bottom) + 2); + } + + left += parseFloat(getComputedStyle(popup).marginLeft); + top += parseFloat(getComputedStyle(popup).marginTop); + + var testid = isSecondTest ? "with mouse" : "anchored to parent frame"; + parent.arguments[0].SimpleTest.is(Math.round(popuprect.left), left, "popup left " + testid); + parent.arguments[0].SimpleTest.is(Math.round(popuprect.top), top, "popup top " + testid); + + document.getElementById("popup").hidePopup(); +} + +function nextTest() +{ + if (isSecondTest) { + parent.arguments[0].SimpleTest.finish(); + parent.close(); + } + else { + // this second test ensures that the popupshowing coordinates when a popup in + // a frame is opened are correct + isSecondTest = true; + synthesizeMouse(document.getElementById("button"), 6, 6, { }); + } +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml new file mode 100644 index 0000000000..f68a06d85d --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe1.xhtml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="frame1" + style="background-color:green;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<iframe + style="margin:10px; min-height:170px; max-width:200px; max-height:200px; border:solid 1px white;" + src="frame_subframe_origin_subframe2.xhtml"></iframe> +<spacer height="3px"/> +<caption id="cap1" style="min-width:200px; max-width:200px; background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame1"), 3, 4, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var el = document.getElementById("cap1"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.arguments[0].SimpleTest.is(e.clientX, 3, "mouse event clientX on sub frame 1"); + parent.arguments[0].SimpleTest.is(e.clientY, 4, "mouse event clientY on sub frame 1"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove", mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml new file mode 100644 index 0000000000..338877ff96 --- /dev/null +++ b/toolkit/content/tests/chrome/frame_subframe_origin_subframe2.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="frame2" + style="background-color:red;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<spacer height="10px"/> +<caption id="cap2" style="background-color:white;" label=""/> +<script class="testbody" type="application/javascript"> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("frame2"), 6, 5, { type: "mousemove" }); +} + +function mouseMove(e) { + e.stopPropagation(); + var el = document.getElementById("cap2"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + parent.parent.arguments[0].SimpleTest.is(e.clientX, 6, "mouse event clientX on sub frame 2"); + parent.parent.arguments[0].SimpleTest.is(e.clientY, 5, "mouse event clientY on sub frame 2"); + parent.parent.arguments[0].SimpleTest.finish(); + parent.parent.close(); +} + +window.addEventListener("mousemove", mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/popup_trigger.js b/toolkit/content/tests/chrome/popup_trigger.js new file mode 100644 index 0000000000..003af044e5 --- /dev/null +++ b/toolkit/content/tests/chrome/popup_trigger.js @@ -0,0 +1,1210 @@ +/* import-globals-from ../widgets/popup_shared.js */ + +var gMenuPopup = null; +var gTrigger = null; +var gIsMenu = false; +var gScreenX = -1, + gScreenY = -1; +var gCachedEvent = null; +var gCachedEvent2 = null; + +function cacheEvent(modifiers) { + var cachedEvent = null; + + var mouseFn = function (event) { + cachedEvent = event; + }; + + window.addEventListener("mousedown", mouseFn); + synthesizeMouse(document.documentElement, 0, 0, modifiers); + window.removeEventListener("mousedown", mouseFn); + + return cachedEvent; +} + +function runTests() { + if (screen.height < 768) { + ok( + false, + "popup tests are likely to fail for screen heights less than 768 pixels" + ); + } + + gMenuPopup = document.getElementById("thepopup"); + gTrigger = document.getElementById("trigger"); + + gIsMenu = gTrigger.hasMenu(); + + // a hacky way to get the screen position of the document. Cache the event + // so that we can use it in calls to openPopup. + gCachedEvent = cacheEvent({ shiftKey: true }); + gScreenX = gCachedEvent.screenX; + gScreenY = gCachedEvent.screenY; + gCachedEvent2 = cacheEvent({ + altKey: true, + ctrlKey: true, + shiftKey: true, + metaKey: true, + }); + + startPopupTests(popupTests); +} + +var popupTests = [ + { + testname: "mouse click on trigger", + events: ["popupshowing thepopup", "popupshown thepopup"], + test() { + // for menus, no trigger will be set. For non-menus using the popup + // attribute, the trigger will be set to the node with the popup attribute + gExpectedTriggerNode = gIsMenu ? "notset" : gTrigger; + synthesizeMouse(gTrigger, 4, 4, {}); + }, + async result(testname) { + gExpectedTriggerNode = null; + // menus are the anchor but non-menus are opened at screen coordinates + is( + gMenuPopup.anchorNode, + gIsMenu ? gTrigger : null, + testname + " anchorNode" + ); + // menus are opened internally, but non-menus have a mouse event which + // triggered them + is( + gMenuPopup.triggerNode, + gIsMenu ? null : gTrigger, + testname + " triggerNode" + ); + + // Popup may have wrong initial size in non e10s mode tests, because + // layout is not yet ready for popup content lazy population on + // popupshowing event. + await new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(r)) + ); + + // this will be used in some tests to ensure the size doesn't change + var popuprect = gMenuPopup.getBoundingClientRect(); + gPopupWidth = Math.round(popuprect.width); + gPopupHeight = Math.round(popuprect.height); + + checkActive(gMenuPopup, "", testname); + checkOpen("trigger", testname); + // if a menu, the popup should be opened underneath the menu in the + // 'after_start' position, otherwise it is opened at the mouse position + if (gIsMenu) { + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } + }, + }, + { + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: ["DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events() { + // No wrapping on menus on Mac + return platformIsMac() + ? [] + : ["DOMMenuItemInactive item1", "DOMMenuItemActive last"]; + }, + test() { + synthesizeKey("KEY_ArrowUp"); + }, + result(testname) { + checkActive(gMenuPopup, platformIsMac() ? "item1" : "last", testname); + }, + }, + { + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + condition() { + return !platformIsMac(); + }, + events: ["DOMMenuItemInactive last", "DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: ["DOMMenuItemInactive item1", "DOMMenuItemActive item2"], + test() { + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "item2", testname); + }, + }, + { + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: ["DOMMenuItemInactive item2", "DOMMenuItemActive item1"], + test() { + synthesizeKey("KEY_ArrowUp"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // cursor left should not do anything + testname: "cursor left", + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // cursor right should not do anything + testname: "cursor right", + test() { + synthesizeKey("KEY_ArrowRight"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // check cursor down when a disabled item exists in the menu + testname: "cursor down disabled", + events() { + // On Windows, disabled items are included when navigating, but on + // other platforms, disabled items are skipped over + if (navigator.platform.indexOf("Win") == 0) { + return ["DOMMenuItemInactive item1", "DOMMenuItemActive item2"]; + } + return ["DOMMenuItemInactive item1", "DOMMenuItemActive amenu"]; + }, + test() { + document.getElementById("item2").disabled = true; + synthesizeKey("KEY_ArrowDown"); + }, + }, + { + // check cursor up when a disabled item exists in the menu + testname: "cursor up disabled", + events() { + if (navigator.platform.indexOf("Win") == 0) { + return [ + "DOMMenuItemInactive item2", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuItemActive item2", + "DOMMenuItemInactive item2", + "DOMMenuItemActive item1", + ]; + } + return ["DOMMenuItemInactive amenu", "DOMMenuItemActive item1"]; + }, + test() { + if (navigator.platform.indexOf("Win") == 0) { + synthesizeKey("KEY_ArrowDown"); + } + synthesizeKey("KEY_ArrowUp"); + if (navigator.platform.indexOf("Win") == 0) { + synthesizeKey("KEY_ArrowUp"); + } + }, + }, + { + testname: "mouse click outside", + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive thepopup", + ], + test() { + gMenuPopup.hidePopup(); + // XXXndeakin event simulation fires events outside of the platform specific + // widget code so the popup capturing isn't handled. Thus, the menu won't + // rollup this way. + // synthesizeMouse(gTrigger, 0, -12, { }); + }, + result(testname, step) { + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + checkClosed("trigger", testname); + }, + }, + { + // these tests check to ensure that passing an anchor and position + // puts the popup in the right place + testname: "open popup anchored", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topleft topleft", + "topcenter topleft", + "topright topleft", + "leftcenter topright", + "rightcenter topright", + "bottomleft bottomleft", + "bottomcenter bottomleft", + "bottomright bottomleft", + "topleft bottomright", + "bottomcenter bottomright", + "rightcenter topright", + "bottomcenter topcenter", + "rightcenter leftcenter", + ], + test(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + // no triggerNode because it was opened without passing an event + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); + }, + }, + { + // these tests check the same but with a 10 pixel margin on the popup + testname: "open popup anchored with margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topleft topleft", + "topcenter topleft", + "topright topleft", + "leftcenter topright", + "rightcenter topright", + "bottomleft bottomleft", + "bottomcenter bottomleft", + "bottomright bottomleft", + "topleft bottomright", + "bottomcenter bottomright", + "rightcenter topright", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: 10px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var rightmod = + step == "before_end" || + step == "after_end" || + step == "start_before" || + step == "start_after" || + step.match(/topright$/) || + step.match(/bottomright$/); + var bottommod = + step == "before_start" || + step == "before_end" || + step == "start_after" || + step == "end_after" || + step.match(/bottomleft$/) || + step.match(/bottomright$/); + compareEdge( + gTrigger, + gMenuPopup, + step, + rightmod ? -10 : 10, + bottommod ? -10 : 10, + testname + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + // these tests check the same but with a -8 pixel margin on the popup + testname: "open popup anchored with negative margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: -8px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var rightmod = + step == "before_end" || + step == "after_end" || + step == "start_before" || + step == "start_after"; + var bottommod = + step == "before_start" || + step == "before_end" || + step == "start_after" || + step == "end_after"; + compareEdge( + gTrigger, + gMenuPopup, + step, + rightmod ? 8 : -8, + bottommod ? 8 : -8, + testname + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "open popup with large positive margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: 1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // as there is more room on the 'end' or 'after' side, popups will always + // appear on the right or bottom corners, depending on which side they are + // allowed to be flipped by. + var expectedleft = + step == "before_end" || step == "after_end" + ? 0 + : Math.round(window.innerWidth - gPopupWidth); + var expectedtop = + step == "start_after" || step == "end_after" + ? 0 + : Math.round(window.innerHeight - gPopupHeight); + is( + Math.round(popuprect.left), + expectedleft, + testname + " x position " + step + ); + is( + Math.round(popuprect.top), + expectedtop, + testname + " y position " + step + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "open popup with large negative margin", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + ], + test(testname, step) { + gMenuPopup.setAttribute("style", "margin: -1000px;"); + gMenuPopup.openPopup(gTrigger, step, 0, 0, false, false); + }, + result(testname, step) { + var popuprect = gMenuPopup.getBoundingClientRect(); + // using negative margins causes the reverse of positive margins, and + // popups will appear on the left or top corners. + var expectedleft = + step == "before_end" || step == "after_end" + ? Math.round(window.innerWidth - gPopupWidth) + : 0; + var expectedtop = + step == "start_after" || step == "end_after" + ? Math.round(window.innerHeight - gPopupHeight) + : 0; + is( + Math.round(popuprect.left), + expectedleft, + testname + " x position " + step + ); + is( + Math.round(popuprect.top), + expectedtop, + testname + " y position " + step + ); + gMenuPopup.removeAttribute("style"); + }, + }, + { + testname: "popup with unknown step", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test() { + gMenuPopup.openPopup(gTrigger, "other", 0, 0, false, false); + }, + result(testname) { + var triggerrect = gMenuPopup.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + triggerrect.left, + testname + " x position " + ); + is(Math.round(popuprect.top), triggerrect.top, testname + " y position "); + }, + }, + { + // these tests check to ensure that the position attribute can be used + // to set the position of a popup instead of passing it as an argument + testname: "open popup anchored with attribute", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + steps: [ + "before_start", + "before_end", + "after_start", + "after_end", + "start_before", + "start_after", + "end_before", + "end_after", + "after_pointer", + "overlap", + "topcenter topleft", + "topright bottomright", + "leftcenter topright", + ], + test(testname, step) { + gMenuPopup.setAttribute("position", step); + gMenuPopup.openPopup(gTrigger, "", 0, 0, false, false); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, step, 0, 0, testname); + }, + }, + { + // this test checks to ensure that the attributes override flag to openPopup + // can be used to override the popup's position. This test also passes an + // event to openPopup to check the trigger node. + testname: "open popup anchored with override", + events: ["popupshowing thepopup 0010", "popupshown thepopup"], + test(testname, step) { + // attribute overrides the position passed in + gMenuPopup.setAttribute("position", "end_after"); + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopup( + gTrigger, + "before_start", + 0, + 0, + false, + true, + gCachedEvent + ); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, gTrigger, testname + " anchorNode"); + is( + gMenuPopup.triggerNode, + gCachedEvent.target, + testname + " triggerNode" + ); + compareEdge(gTrigger, gMenuPopup, "end_after", 0, 0, testname); + }, + }, + { + testname: "close popup with escape", + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + synthesizeKey("KEY_Escape"); + checkClosed("trigger", testname); + }, + }, + { + // check that offsets may be supplied to the openPopup method + testname: "open popup anchored with offsets", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + // attribute is empty so does not override + gMenuPopup.setAttribute("position", ""); + gMenuPopup.openPopup(gTrigger, "before_start", 5, 10, true, true); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "before_start", 5, 10, testname); + }, + }, + { + // if no anchor is supplied to openPopup, it should be opened relative + // to the viewport. + testname: "open popup unanchored", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gMenuPopup.openPopup(null, "after_start", 6, 8, false); + }, + result(testname, step) { + var rect = gMenuPopup.getBoundingClientRect(); + ok( + rect.left == 6 && rect.top == 8 && rect.right && rect.bottom, + testname + ); + }, + }, + { + testname: "activate menuitem with mouse", + events: [ + "DOMMenuInactive thepopup", + "command item3", + "popuphiding thepopup", + "popuphidden thepopup", + ], + test(testname, step) { + var item3 = document.getElementById("item3"); + synthesizeMouse(item3, 4, 4, {}); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "close popup", + condition() { + return false; + }, + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + gMenuPopup.hidePopup(); + }, + }, + { + testname: "open popup at screen", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gExpectedTriggerNode = "notset"; + gMenuPopup.openPopupAtScreen(gScreenX + 24, gScreenY + 20, false); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is(gMenuPopup.triggerNode, null, testname + " triggerNode"); + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, 24, testname + " left"); + is(rect.top, 20, testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + }, + }, + { + // check that pressing a menuitem's accelerator selects it. Note that + // the menuitem with the M accesskey overrides the earlier menuitem that + // begins with M. + testname: "menuitem accelerator", + events: [ + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive thepopup", + "command amenu", + "popuphiding thepopup", + "popuphidden thepopup", + ], + test() { + sendString("M"); + }, + result(testname) { + checkClosed("trigger", testname); + }, + }, + { + testname: "open context popup at screen", + events: ["popupshowing thepopup 0010", "popupshown thepopup"], + test(testname, step) { + gExpectedTriggerNode = gCachedEvent.target; + gMenuPopup.openPopupAtScreen( + gScreenX + 8, + gScreenY + 16, + true, + gCachedEvent + ); + }, + result(testname, step) { + gExpectedTriggerNode = null; + is(gMenuPopup.anchorNode, null, testname + " anchorNode"); + is( + gMenuPopup.triggerNode, + gCachedEvent.target, + testname + " triggerNode" + ); + + var openX = 8; + var openY = 16; + var rect = gMenuPopup.getBoundingClientRect(); + is(rect.left, openX + (platformIsMac() ? 1 : 2), testname + " left"); + is(rect.top, openY + (platformIsMac() ? -6 : 2), testname + " top"); + ok(rect.right, testname + " right is " + rect.right); + ok(rect.bottom, testname + " bottom is " + rect.bottom); + }, + }, + { + // pressing a letter that doesn't correspond to an accelerator, but does + // correspond to the first letter in a menu's label. The menu should not + // close because there is more than one item corresponding to that letter + testname: "menuitem with non accelerator", + events: ["DOMMenuItemActive one"], + test() { + sendString("O"); + }, + result(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "one", testname); + }, + }, + { + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with non accelerator again", + events: ["DOMMenuItemInactive one", "DOMMenuItemActive submenu"], + test() { + sendString("O"); + }, + result(testname) { + // 'submenu' is a menu but it should not be open + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + }, + }, + { + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenuitem", + "popupshown submenupopup", + ], + test() { + synthesizeKey("KEY_ArrowRight"); + }, + result(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive( + document.getElementById("submenupopup"), + "submenuitem", + testname + ); + }, + }, + { + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", + "DOMMenuInactive submenupopup", + ], + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + }, + }, + { + // open the submenu with the enter key + testname: "open submenu with enter", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenuitem", + "popupshown submenupopup", + ], + test() { + synthesizeKey("KEY_Enter"); + }, + result(testname) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive( + document.getElementById("submenupopup"), + "submenuitem", + testname + ); + }, + }, + { + // close the submenu with the escape key + testname: "close submenu with escape", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "DOMMenuItemInactive submenuitem", + "DOMMenuInactive submenupopup", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + checkOpen("trigger", testname); + checkClosed("submenu", testname); + checkActive(gMenuPopup, "submenu", testname); + checkActive(document.getElementById("submenupopup"), "", testname); + }, + }, + { + // pressing the letter again when the next item is disabled should still + // select the disabled item on Windows, but select the next item on other + // platforms + testname: "menuitem with non accelerator disabled", + events() { + if (navigator.platform.indexOf("Win") == 0) { + return [ + "DOMMenuItemInactive submenu", + "DOMMenuItemActive other", + "DOMMenuItemInactive other", + "DOMMenuItemActive item1", + ]; + } + return [ + "DOMMenuItemInactive submenu", + "DOMMenuItemActive last", + "DOMMenuItemInactive last", + "DOMMenuItemActive item1", + ]; + }, + test() { + sendString("OF"); + }, + result(testname) { + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // pressing a letter that doesn't correspond to an accelerator nor the + // first letter of a menu. This should have no effect. + testname: "menuitem with keypress no accelerator found", + test() { + sendString("G"); + }, + result(testname) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "item1", testname); + }, + }, + { + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with non accelerator single", + events: [ + "DOMMenuItemInactive item1", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive thepopup", + "command amenu", + "popuphiding thepopup", + "popuphidden thepopup", + ], + test() { + sendString("M"); + }, + result(testname) { + checkClosed("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "open context popup at screen with all modifiers set", + events: ["popupshowing thepopup 1111", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopupAtScreen( + gScreenX + 8, + gScreenY + 16, + true, + gCachedEvent2 + ); + }, + }, + { + testname: "open popup with open property", + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + openMenu(gTrigger); + }, + result(testname, step) { + checkOpen("trigger", testname); + if (gIsMenu) { + compareEdge(gTrigger, gMenuPopup, "after_start", 0, 0, testname); + } + }, + }, + { + testname: "open submenu with open property", + events: [ + "popupshowing submenupopup", + "DOMMenuItemActive submenu", + "popupshown submenupopup", + ], + test(testname, step) { + openMenu(document.getElementById("submenu")); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkOpen("submenu", testname); + // XXXndeakin + // getBoundingClientRect doesn't seem to working right for submenus + // so disable this test for now + // compareEdge(document.getElementById("submenu"), + // document.getElementById("submenupopup"), "end_before", 0, 0, testname); + }, + }, + { + testname: "hidePopup hides entire chain", + events: [ + "popuphiding submenupopup", + "popuphidden submenupopup", + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive submenupopup", + "DOMMenuItemInactive submenu", + "DOMMenuInactive thepopup", + ], + test() { + gMenuPopup.hidePopup(); + }, + result(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + }, + }, + { + testname: "open submenu with open property without parent open", + test(testname, step) { + openMenu(document.getElementById("submenu")); + }, + result(testname, step) { + checkClosed("trigger", testname); + checkClosed("submenu", testname); + }, + }, + { + testname: "open popup with open property and position", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gMenuPopup.setAttribute("position", "before_start"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "before_start", 0, 0, testname); + }, + }, + { + testname: "close popup with open property", + condition() { + return gIsMenu; + }, + events: [ + "popuphiding thepopup", + "popuphidden thepopup", + "DOMMenuInactive thepopup", + ], + test(testname, step) { + closeMenu(gTrigger, gMenuPopup); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "open popup with open property, position, anchor and alignment", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.setAttribute("position", "start_after"); + gMenuPopup.setAttribute("popupanchor", "topright"); + gMenuPopup.setAttribute("popupalign", "bottomright"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "start_after", 0, 0, testname); + }, + }, + { + testname: "open popup with open property, anchor and alignment", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.removeAttribute("position"); + gMenuPopup.setAttribute("popupanchor", "bottomright"); + gMenuPopup.setAttribute("popupalign", "topright"); + openMenu(gTrigger); + }, + result(testname, step) { + compareEdge(gTrigger, gMenuPopup, "after_end", 0, 0, testname); + gMenuPopup.removeAttribute("popupanchor"); + gMenuPopup.removeAttribute("popupalign"); + }, + }, + { + testname: "focus and cursor down on trigger", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gTrigger.focus(); + synthesizeKey("KEY_ArrowDown", { altKey: !platformIsMac() }); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "focus and cursor up on trigger", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + test(testname, step) { + gTrigger.focus(); + synthesizeKey("KEY_ArrowUp", { altKey: !platformIsMac() }); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + testname: "select and enter on menuitem", + condition() { + return gIsMenu; + }, + events: [ + "DOMMenuItemActive item1", + "DOMMenuItemInactive item1", + "DOMMenuInactive thepopup", + "command item1", + "popuphiding thepopup", + "popuphidden thepopup", + ], + test(testname, step) { + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "focus trigger and key to open", + condition() { + return gIsMenu; + }, + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gTrigger.focus(); + synthesizeKey(platformIsMac() ? " " : "KEY_F4"); + }, + result(testname, step) { + checkOpen("trigger", testname); + checkActive(gMenuPopup, "", testname); + }, + }, + { + // the menu should only open when the meta or alt key is not pressed + testname: "focus trigger and key wrong modifier", + condition() { + return gIsMenu; + }, + test(testname, step) { + gTrigger.focus(); + if (platformIsMac()) { + synthesizeKey("KEY_F4", { altKey: true }); + } else { + synthesizeKey("", { metaKey: true }); + } + }, + result(testname, step) { + checkClosed("trigger", testname); + }, + }, + { + testname: "mouse click on disabled menu", + condition() { + return gIsMenu; + }, + test(testname, step) { + gTrigger.setAttribute("disabled", "true"); + synthesizeMouse(gTrigger, 4, 4, {}); + }, + result(testname, step) { + checkClosed("trigger", testname); + gTrigger.removeAttribute("disabled"); + }, + }, + { + // openPopup using object as position argument + testname: "openPopup with object argument", + events: ["popupshowing thepopup 0000", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(gTrigger, { position: "before_start", x: 5, y: 7 }); + checkOpen("trigger", testname); + }, + result(testname, step) { + var triggerrect = gTrigger.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + Math.round(triggerrect.left + 5), + testname + " x position " + ); + is( + Math.round(popuprect.bottom), + Math.round(triggerrect.top + 7), + testname + " y position " + ); + }, + }, + { + testname: "openPopup with object argument with event", + events: ["popupshowing thepopup 1000", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(gTrigger, { + position: "after_start", + x: 0, + y: 0, + triggerEvent: new MouseEvent("mousedown", { altKey: true }), + }); + checkOpen("trigger", testname); + }, + }, + { + testname: "openPopup with no arguments", + events: ["popupshowing thepopup", "popupshown thepopup"], + autohide: "thepopup", + test(testname, step) { + gMenuPopup.openPopup(); + }, + result(testname, step) { + let isMenu = gTrigger.type == "menu"; + // With no arguments, open in default menu position + var triggerrect = gTrigger.getBoundingClientRect(); + var popuprect = gMenuPopup.getBoundingClientRect(); + is( + Math.round(popuprect.left), + isMenu ? Math.round(triggerrect.left) : 0, + testname + " x position " + ); + is( + Math.round(popuprect.top), + isMenu ? Math.round(triggerrect.bottom) : 0, + testname + " y position " + ); + }, + }, + { + // openPopup should open the menu synchronously, however popupshown + // is fired asynchronously + testname: "openPopup synchronous", + events: [ + "popupshowing thepopup", + "popupshowing submenupopup", + "popupshown thepopup", + "DOMMenuItemActive submenu", + "popupshown submenupopup", + ], + test(testname, step) { + gMenuPopup.openPopup(gTrigger, "after_start", 0, 0, false, true); + document + .getElementById("submenupopup") + .openPopup(gTrigger, "end_before", 0, 0, false, true); + checkOpen("trigger", testname); + checkOpen("submenu", testname); + }, + }, + { + // remove the content nodes for the popup + testname: "remove content", + test(testname, step) { + var submenupopup = document.getElementById("submenupopup"); + submenupopup.remove(); + var popup = document.getElementById("thepopup"); + popup.remove(); + }, + }, +]; + +function platformIsMac() { + return navigator.platform.indexOf("Mac") > -1; +} diff --git a/toolkit/content/tests/chrome/sample_entireword_latin1.html b/toolkit/content/tests/chrome/sample_entireword_latin1.html new file mode 100644 index 0000000000..b2d66fa3c4 --- /dev/null +++ b/toolkit/content/tests/chrome/sample_entireword_latin1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head><title>Latin entire-word find test page</title></head> + <body> + <!-- Feel free to extend the contents of this page with more comprehensive + - Latin punctuation and/ or word markers. + --> + <p>The twins of Mammon quarrelled. Their warring plunged the world into a new darkness, and the beast abhorred the darkness. So it began to move swiftly, and grew more powerful, and went forth and multiplied. And the beasts brought fire and light to the darkness.</p> + <p>from The Book of Mozilla, 15:1</p> + </body> +</html> diff --git a/toolkit/content/tests/chrome/test_about_networking.html b/toolkit/content/tests/chrome/test_about_networking.html new file mode 100644 index 0000000000..5465c07751 --- /dev/null +++ b/toolkit/content/tests/chrome/test_about_networking.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=912103 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug </title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var dashboard = Cc["@mozilla.org/network/dashboard;1"] + .getService(Ci.nsIDashboard); + dashboard.enableLogging = true; + + var wsURI = "ws://mochi.test:8888/chrome/toolkit/content/tests/chrome/file_about_networking"; + var websocket = new WebSocket(wsURI); + + websocket.addEventListener("open", function() { + dashboard.requestWebsocketConnections(function(data) { + var found = false; + for (var i = 0; i < data.websockets.length; i++) { + if (data.websockets[i].hostport == "mochi.test:8888") { + found = true; + break; + } + } + isnot(found, false, "tested websocket entry not found"); + websocket.close(); + SimpleTest.finish(); + }); + }); + } + + window.addEventListener("DOMContentLoaded", function() { + runTest(); + }, {once: true}); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=912103">Mozilla Bug </a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_arrowpanel.xhtml b/toolkit/content/tests/chrome/test_arrowpanel.xhtml new file mode 100644 index 0000000000..cd8d312e1d --- /dev/null +++ b/toolkit/content/tests/chrome/test_arrowpanel.xhtml @@ -0,0 +1,332 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Arrow Panels" + style="padding: 10px;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<stack flex="1"> + <label id="topleft" value="Top Left Corner" style="justify-self: left; margin-left: 15px; align-self: start; margin-top: 15px;"/> + <label id="topright" value="Top Right" style="justify-self: right; margin-right: 15px; align-self: start; margin-top: 15px;"/> + <label id="bottomleft" value="Bottom Left Corner" style="justify-self: left; margin-left: 15px; align-self: end; margin-bottom: 15px;"/> + <label id="bottomright" value="Bottom Right" style="justify-self: right; margin-right: 15px; align-self: end; margin-bottom: 15px;"/> + <!-- Our SimpleTest/TestRunner.js runs tests inside an iframe which sizes are W=500 H=300. + 'left' and 'top' values need to be set so that the panel (popup) has enough room to display on its 4 sides. --> + <label id="middle" value="+/- Centered" style="justify-self: left; margin-left: 225px; align-self: start; margin-top: 135px;"/> + <iframe id="frame" type="content" + src="data:text/html,<input id='input'>" style="width: 100px; height: 100px; justify-self: left; margin-left: 225px; align-self: start; margin-top: 120px;"/> +</stack> + +<panel id="panel" type="arrow" animate="false" + onpopupshown="checkPanelPosition(this)" onpopuphidden="runNextTest.next()"> + <box style="width: 115px; height: 65px"/> +</panel> + +<panel id="bigpanel" type="arrow" animate="false" + onpopupshown="checkBigPanel(this)" onpopuphidden="runNextTest.next()"> + <box style="width: 125px; height: 3000px"/> +</panel> + +<panel id="animatepanel" type="arrow" + onpopupshown="animatedPopupShown = true;" + onpopuphidden="animatedPopupHidden = true; runNextTest.next();"> + <label value="Animate Closed" style="height: 40px"/> +</panel> + +<html:style type="text/css"> + panel { + /** + * We hardcode a panel padding here to avoid rounding issues caused by + * using em unit padding, which is the default as of bug 1701920. + */ + --arrowpanel-padding: 16px; + /** + * Linux and windows have some negative margin-inline that can change the + * overflow calculations + */ + margin-inline: 0 !important; + } +</html:style> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var expectedAnchor = null; +var expectedSide = "", expectedAnchorEdge = ""; +var zoomFactor = 1; +var animatedPopupShown = false; +var animatedPopupHidden = false; +var runNextTest; + +function startTest() +{ + runNextTest = nextTest(); + runNextTest.next(); +} + +function* nextTest() +{ + var panel = $("panel"); + + function openPopup(position, anchor, expected, anchorEdge) + { + expectedAnchor = Node.isInstance(anchor) ? anchor : $(anchor); + expectedSide = expected; + expectedAnchorEdge = anchorEdge; + + panel.removeAttribute("side"); + panel.openPopup(expectedAnchor, position, 0, 0, false, false, null); + } + + for (var iter = 0; iter < 2; iter++) { + openPopup("after_start", "topleft", "top", "left"); + yield; + openPopup("after_start", "bottomleft", "bottom", "left"); + yield; + openPopup("before_start", "topleft", "top", "left"); + yield; + openPopup("before_start", "bottomleft", "bottom", "left"); + yield; + openPopup("after_start", "middle", "top", "left"); + yield; + openPopup("before_start", "middle", "bottom", "left"); + yield; + + openPopup("after_start", "topright", "top", "right"); + yield; + openPopup("after_start", "bottomright", "bottom", "right"); + yield; + openPopup("before_start", "topright", "top", "right"); + yield; + openPopup("before_start", "bottomright", "bottom", "right"); + yield; + + openPopup("after_end", "middle", "top", "right"); + yield; + openPopup("before_end", "middle", "bottom", "right"); + yield; + + openPopup("start_before", "topleft", "left", "top"); + yield; + openPopup("start_before", "topright", "right", "top"); + yield; + openPopup("end_before", "topleft", "left", "top"); + yield; + openPopup("end_before", "topright", "right", "top"); + yield; + openPopup("start_before", "middle", "right", "top"); + yield; + openPopup("end_before", "middle", "left", "top"); + yield; + + openPopup("start_before", "bottomleft", "left", "bottom"); + yield; + openPopup("start_before", "bottomright", "right", "bottom"); + yield; + openPopup("end_before", "bottomleft", "left", "bottom"); + yield; + openPopup("end_before", "bottomright", "right", "bottom"); + yield; + + openPopup("start_after", "middle", "right", "bottom"); + yield; + openPopup("end_after", "middle", "left", "bottom"); + yield; + + openPopup("topcenter bottomleft", "bottomleft", "bottom", "center left"); + yield; + openPopup("bottomcenter topleft", "topleft", "top", "center left"); + yield; + openPopup("topcenter bottomright", "bottomright", "bottom", "center right"); + yield; + openPopup("bottomcenter topright", "topright", "top", "center right"); + yield; + openPopup("topcenter bottomleft", "middle", "bottom", "center left"); + yield; + openPopup("bottomcenter topleft", "middle", "top", "center left"); + yield; + + openPopup("leftcenter topright", "middle", "right", "center top"); + yield; + openPopup("rightcenter bottomleft", "middle", "left", "center bottom"); + yield; + +/* + XXXndeakin disable these parts of the test which often cause problems, see bug 626563 + + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left"); + yield; + + setScale(frames[0], 1.5); + openPopup("after_start", frames[0].document.getElementById("input"), "top", "left"); + yield; + + setScale(frames[0], 2.5); + openPopup("before_start", frames[0].document.getElementById("input"), "bottom", "left"); + yield; + + setScale(frames[0], 1); +*/ + + $("bigpanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + yield; + + // switch to rtl mode + document.documentElement.style.direction = "rtl"; + + $("topleft").style.marginRight = "15px"; + $("topleft").style.justifySelf = "right"; + + $("topright").style.marginLeft = "15px"; + $("topright").style.justifySelf = "left"; + + $("bottomleft").style.marginRight = "15px"; + $("bottomleft").style.justifySelf = "right"; + + $("bottomright").style.marginLeft = "15px"; + $("bottomright").style.justifySelf = "left"; + + $("topleft").style.removeProperty("margin-left"); + $("topright").style.removeProperty("margin-right"); + $("bottomleft").style.removeProperty("margin-left"); + $("bottomright").style.removeProperty("margin-right"); + } + + // Test that a transition occurs when opening or closing the popup. + if (matchMedia("(-moz-panel-animations").matches) { + function transitionEnded(event) { + if ($("animatepanel").state != "open") { + is($("animatepanel").state, "showing", "state is showing during transitionend"); + ok(!animatedPopupShown, "popupshown not fired yet") + } else { + is($("animatepanel").state, "open", "state is open after transitionend"); + ok(animatedPopupShown, "popupshown now fired") + SimpleTest.executeSoon(() => runNextTest.next()); + } + } + + // Check that the transition occurs for an arrow panel with animate="true" + $("animatepanel").addEventListener("transitionend", transitionEnded); + $("animatepanel").openPopup($("topleft"), "after_start", 0, 0, false, false, null, "start"); + is($("animatepanel").state, "showing", "state is showing"); + yield; + $("animatepanel").removeEventListener("transitionend", transitionEnded); + + synthesizeKey("KEY_Escape"); + ok(!animatedPopupHidden, "animated popup not hidden yet"); + yield; + } + + SimpleTest.finish() +} + +function setScale(win, scale) +{ + SpecialPowers.setFullZoom(win, scale); + zoomFactor = scale; +} + +function checkPanelPosition(panel) +{ + let anchor = panel.anchorNode; + let adj = 0, hwinpos = 0, vwinpos = 0; + if (anchor.ownerDocument != document) { + var framerect = anchor.ownerGlobal.frameElement.getBoundingClientRect(); + hwinpos = framerect.left; + vwinpos = framerect.top; + } + + // Positions are reversed in rtl yet the coordinates used in the computations + // are not, so flip the expected label side and anchor edge. + var isRTL = (window.getComputedStyle(panel).direction == "rtl"); + if (isRTL) { + var flipLeftRight = val => val == "left" ? "right" : "left"; + expectedAnchorEdge = expectedAnchorEdge.replace(/(left|right)/, flipLeftRight); + expectedSide = expectedSide.replace(/(left|right)/, flipLeftRight); + } + + var panelRect = panel.getBoundingClientRect(); + var anchorRect = anchor.getBoundingClientRect(); + var contentRect = panel.firstChild.getBoundingClientRect(); + switch (expectedSide) { + case "top": + ok(contentRect.top > vwinpos + anchorRect.bottom * zoomFactor + 5, "panel content is below"); + break; + case "bottom": + ok(contentRect.bottom < vwinpos + anchorRect.top * zoomFactor - 5, "panel content is above"); + break; + case "left": + ok(contentRect.left > hwinpos + anchorRect.right * zoomFactor + 5, "panel content is right"); + break; + case "right": + ok(contentRect.right < hwinpos + anchorRect.left * zoomFactor - 5, "panel content is left"); + break; + } + + let desc = panel.id + ": anchored on " + expectedAnchorEdge + " to " + anchor.id + " | " + (isRTL ? "rtl" : "ltr") + " | " + anchor.getAttribute("style"); + let iscentered = false; + if (expectedAnchorEdge.indexOf("center ") == 0) { + expectedAnchorEdge = expectedAnchorEdge.substring(7); + iscentered = true; + } + + switch (expectedAnchorEdge) { + case "top": + adj = vwinpos + parseInt(getComputedStyle(panel).marginTop); + if (iscentered) + adj += anchorRect.height / 2; + isWithinHalfPixel(panelRect.top, anchorRect.top * zoomFactor + adj, desc); + break; + case "bottom": + adj = vwinpos + parseInt(getComputedStyle(panel).marginBottom); + if (iscentered) + adj += anchorRect.height / 2; + isWithinHalfPixel(panelRect.bottom, anchorRect.bottom * zoomFactor - adj, desc); + break; + case "left": + adj = hwinpos + parseInt(getComputedStyle(panel).marginLeft); + if (iscentered) + adj += anchorRect.width / 2; + isWithinHalfPixel(panelRect.left, anchorRect.left * zoomFactor + adj, desc); + break; + case "right": + adj = hwinpos + parseInt(getComputedStyle(panel).marginRight); + if (iscentered) + adj += anchorRect.width / 2; + isWithinHalfPixel(panelRect.right, anchorRect.right * zoomFactor - adj, desc); + break; + } + + is(anchor, expectedAnchor, "anchor"); + + is(panel.getAttribute("side"), expectedSide, "panel arrow side"); + + panel.hidePopup(); +} + +function isWithinHalfPixel(a, b, desc) +{ + ok(Math.abs(a - b) <= 0.5, `${desc}: ${a} vs. ${b}`); +} + +function checkBigPanel(panel) +{ + ok(panel.getBoundingClientRect().height < screen.height, "big panel height"); + panel.hidePopup(); +} + +SimpleTest.waitForFocus(startTest); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"/> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete2.xhtml b/toolkit/content/tests/chrome/test_autocomplete2.xhtml new file mode 100644 index 0000000000..844305c321 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete2.xhtml @@ -0,0 +1,189 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 2" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = false; + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAtn() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +var element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkResult(); + }; +}(element.onSearchComplete); + +// Test Bug 441530 - correctly setting "nomatch" +// Test Bug 441526 - correctly setting style with "highlightnonmatches" + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + var autocomplete = $("autocomplete"); + + // Ensure highlightNonMatches can be set correctly. + + // This should not be set by default. + is(autocomplete.hasAttribute("highlightnonmatches"), false, + "highlight nonmatches not set by default"); + + autocomplete.highlightNonMatches = "true"; + + is(autocomplete.getAttribute("highlightnonmatches"), "true", + "highlight non matches attribute set correctly"); + is(autocomplete.highlightNonMatches, true, + "highlight non matches getter returned correctly"); + + autocomplete.highlightNonMatches = "false"; + + is(autocomplete.getAttribute("highlightnonmatches"), "false", + "highlight non matches attribute set to false correctly"); + is(autocomplete.highlightNonMatches, false, + "highlight non matches getter returned false correctly"); + + check(); +} + +function check() { + var autocomplete = $("autocomplete"); + + // Toggle this value, so we can re-use the one function. + returnResult = !returnResult; + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + var style = window.getComputedStyle(autocomplete); + + if (returnResult) { + // Result was returned, so there should not be a nomatch attribute + is(autocomplete.hasAttribute("nomatch"), false, + "nomatch attribute shouldn't be present here"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + isnot(style.color, "rgb(255, 0, 0)", + "not nomatch and highlightNonMatches - should not be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.color, "rgb(255, 0, 0)", + "not nomatch and not highlightNonMatches - should not be red"); + + setTimeout(check, 0); + } + else { + // No result was returned, so there should be nomatch attribute + is(autocomplete.getAttribute("nomatch"), "true", + "nomatch attribute not correctly set when expected"); + + // Ensure that the style is set correctly whichever way highlightNonMatches + // is set. + autocomplete.highlightNonMatches = "true"; + + is(style.color, "rgb(255, 0, 0)", + "nomatch and highlightNonMatches - should be red"); + + autocomplete.highlightNonMatches = "false"; + + isnot(style.color, "rgb(255, 0, 0)", + "nomatch and not highlightNonMatches - should not be red"); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete3.xhtml b/toolkit/content/tests/chrome/test_autocomplete3.xhtml new file mode 100644 index 0000000000..a1b9ef84ea --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete3.xhtml @@ -0,0 +1,200 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 3" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkResult(); + }; +}(element.onSearchComplete); + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +var currentTest = 0; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { completeDefaultIndex: "false", key: "r", result: "r", + start: 1, end: 1 }, + { completeDefaultIndex: "true", key: "e", result: "result", + start: 2, end: 6 }, + { completeDefaultIndex: "true", key: "t", result: "ret >> Result", + start: 3, end: 13 } +]; + +function startTest() { + var autocomplete = $("autocomplete"); + + // These should not be set by default. + is(autocomplete.hasAttribute("completedefaultindex"), false, + "completedefaultindex not set by default"); + + autocomplete.completeDefaultIndex = "true"; + + is(autocomplete.getAttribute("completedefaultindex"), "true", + "completedefaultindex attribute set correctly"); + is(autocomplete.completeDefaultIndex, true, + "autoFill getter returned correctly"); + + autocomplete.completeDefaultIndex = "false"; + + is(autocomplete.getAttribute("completedefaultindex"), "false", + "completedefaultindex attribute set to false correctly"); + is(autocomplete.completeDefaultIndex, false, + "completeDefaultIndex getter returned false correctly"); + + checkNext(); +} + +function checkNext() { + var autocomplete = $("autocomplete"); + + autocomplete.completeDefaultIndex = tests[currentTest].completeDefaultIndex; + autocomplete.focus(); + + synthesizeKey(tests[currentTest].key); +} + +function checkResult() { + var autocomplete = $("autocomplete"); + + is(autocomplete.value, tests[currentTest].result, + "Test " + currentTest + ": autocomplete.value should equal '" + + tests[currentTest].result + "'"); + + is(autocomplete.selectionStart, tests[currentTest].start, + "Test " + currentTest + ": autocomplete selection should start at " + + tests[currentTest].start); + + is(autocomplete.selectionEnd, tests[currentTest].end, + "Test " + currentTest + ": autocomplete selection should end at " + + tests[currentTest].end); + + ++currentTest; + + if (currentTest < tests.length) { + setTimeout(checkNext, 0); + } else { + // TODO (bug 494809): Autocomplete-in-the-middle should take in count RTL + // and complete on KEY_ArrowRight or KEY_ArrowLeft based on that. It should also revert + // what user has typed to far if he moves in the opposite direction. + if (!autocomplete.value.includes(">>")) { + // Test result if user accepts autocomplete suggestion. + synthesizeKey("KEY_ArrowRight"); + is( + autocomplete.value, + "Result", + "Test complete: autocomplete.value should equal 'Result'" + ); + is( + autocomplete.selectionStart, + 6, + "Test complete: autocomplete selection should start at 6" + ); + is( + autocomplete.selectionEnd, + 6, + "Test complete: autocomplete selection should end at 6" + ); + } + + setTimeout(function () { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory( + autoCompleteSimpleID, + autoCompleteSimple + ); + SimpleTest.finish(); + }, 0); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete4.xhtml b/toolkit/content/tests/chrome/test_autocomplete4.xhtml new file mode 100644 index 0000000000..bb16194e55 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete4.xhtml @@ -0,0 +1,280 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 4" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + completedefaultindex="true" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +// Set to indicate whether or not we want autoCompleteSimple to return a result +var returnResult = true; + +const IS_MAC = navigator.platform.includes("Mac"); + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + if (returnResult) { + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "Result"; + } +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + searchComplete(); + }; +}(element.onSearchComplete); + +// Test Bug 325842 - completeDefaultIndex + +SimpleTest.waitForExplicitFinish(); + +setTimeout(nextTest, 0); + +var currentTest = null; + +// Note the entries for these tests (key) are incremental. +const tests = [ + { + desc: "HOME key remove selection", + key: "KEY_Home", + removeSelection: true, + result: "re", + start: 0, end: 0 + }, + { + desc: "LEFT key remove selection", + key: "KEY_ArrowLeft", + removeSelection: true, + result: "re", + start: 1, end: 1 + }, + { desc: "RIGHT key remove selection", + key: "KEY_ArrowRight", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { desc: "ENTER key remove selection", + key: "KEY_Enter", + removeSelection: true, + result: "re", + start: 2, end: 2 + }, + { + desc: "HOME key", + key: "KEY_Home", + removeSelection: false, + result: "Result", + start: 0, end: 0 + }, + { + desc: "LEFT key", + key: "KEY_ArrowLeft", + removeSelection: false, + result: "Result", + start: 5, end: 5 + }, + { desc: "RIGHT key", + key: "KEY_ArrowRight", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "RETURN key", + key: "KEY_Enter", + removeSelection: false, + result: "Result", + start: 6, end: 6 + }, + { desc: "TAB key should confirm suggestion when forcecomplete is set", + key: "KEY_Tab", + removeSelection: false, + forceComplete: true, + result: "Result", + start: 6, end: 6 + }, + + { desc: "RIGHT key complete from middle", + key: "KEY_ArrowRight", + forceComplete: true, + completeFromMiddle: true, + result: "Result", + start: 6, end: 6 + }, + { + desc: "RIGHT key w/ minResultsForPopup=2", + key: "KEY_ArrowRight", + removeSelection: false, + minResultsForPopup: 2, + result: "Result", + start: 6, end: 6 + }, +]; + +function nextTest() { + if (!tests.length) { + // No more tests to run, finish. + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); + return; + } + + var autocomplete = $("autocomplete"); + autocomplete.value = ""; + currentTest = tests.shift(); + + // HOME key works differently on Mac, so we skip tests using it. + if (currentTest.key == "KEY_Home" && IS_MAC) + nextTest(); + else + setTimeout(runCurrentTest, 0); +} + +function runCurrentTest() { + var autocomplete = $("autocomplete"); + if ("minResultsForPopup" in currentTest) + autocomplete.setAttribute("minresultsforpopup", currentTest.minResultsForPopup) + else + autocomplete.removeAttribute("minresultsforpopup"); + + autocomplete.focus(); + + if (!currentTest.completeFromMiddle) { + sendString("re"); + } + else { + sendString("lt"); + } +} + +function searchComplete() { + var autocomplete = $("autocomplete"); + autocomplete.setAttribute("forcecomplete", currentTest.forceComplete); + + if (currentTest.completeFromMiddle) { + if (!currentTest.forceComplete) { + synthesizeKey(currentTest.key); + } + else if (!/ >> /.test(autocomplete.value)) { + // At this point we should have a value like "lt >> Result" showing. + throw new Error("Expected an middle-completed value, got " + autocomplete.value); + } + + // For forceComplete a blur should cause a value from the results to get + // completed to. E.g. "lt >> Result" will turn into "Result". + if (currentTest.forceComplete) + autocomplete.blur(); + + checkResult(); + return; + } + + is(autocomplete.value, "result", + "Test '" + currentTest.desc + "': autocomplete.value should equal 'result'"); + + if (autocomplete.selectionStart == 2) { // Finished inserting "re" string. + if (currentTest.removeSelection) { + // remove current selection + synthesizeKey("KEY_Delete"); + } + + synthesizeKey(currentTest.key); + + checkResult(); + } +} + +function checkResult() { + var autocomplete = $("autocomplete"); + + is(autocomplete.value, currentTest.result, + "Test '" + currentTest.desc + "': autocomplete.value should equal '" + + currentTest.result + "'"); + + is(autocomplete.selectionStart, currentTest.start, + "Test '" + currentTest.desc + "': autocomplete selection should start at " + + currentTest.start); + + is(autocomplete.selectionEnd, currentTest.end, + "Test '" + currentTest.desc + "': autocomplete selection should end at " + + currentTest.end); + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete5.xhtml b/toolkit/content/tests/chrome/test_autocomplete5.xhtml new file mode 100644 index 0000000000..7c252f355e --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete5.xhtml @@ -0,0 +1,164 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test 5" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + autocompletesearch="simple" + notifylegacyevents="true"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +const ACR = Ci.nsIAutoCompleteResult; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; + this._param = "SUCCESS"; +} + +nsAutoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that either returns one result or none +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + + +let element = document.getElementById("autocomplete"); + +// Create stub to intercept `onSearchBegin` event. +element.onSearchBegin = function(original) { + return function() { + original.apply(this, arguments); + checkSearchBegin(); + }; +}(element.onSearchBegin); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkSearchCompleted(); + }; +}(element.onSearchComplete); + +element.addEventListener("textEntered", checkTextEntered); +element.addEventListener("textReverted", checkTextReverted); + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +SimpleTest.waitForExplicitFinish(); +setTimeout(startTest, 0); + +function startTest() { + let autocomplete = $("autocomplete"); + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +let hasTextEntered = false; +let hasSearchBegun = false; + +function checkSearchBegin() { + hasSearchBegun = true; +} + +let test = 0; +function checkSearchCompleted() { + is(hasSearchBegun, true, "onsearchbegin handler has been correctly called."); + + if (test == 0) { + hasSearchBegun = false; + synthesizeKey("KEY_Enter"); + } else if (test == 1) { + hasSearchBegun = false; + synthesizeKey("KEY_Escape"); + } else { + throw new Error("checkSearchCompleted should only be called twice."); + } +} + +function checkTextEntered() { + is(test, 0, "checkTextEntered should be reached from first test."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + // fire second test + test++; + + let autocomplete = $("autocomplete"); + autocomplete.textValue = ""; + autocomplete.blur(); + autocomplete.focus(); + sendString("r"); +} + +function checkTextReverted() { + is(test, 1, "checkTextReverted should be the second test reached."); + is(hasSearchBegun, false, "onsearchbegin handler should not be called on text revert."); + + setTimeout(function() { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + }, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml new file mode 100644 index 0000000000..20eb96323f --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete emphasis test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="richautocomplete" + is="autocomplete-input" + autocompletesearch="simple" + autocompletepopup="richpopup"/> +<panel is="autocomplete-richlistbox-popup" + id="richpopup" + type="autocomplete-richlistbox" + noautofocus="true"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +const ACR = Ci.nsIAutoCompleteResult; + +// A global variable to hold the search result for the current search. +var resultText = ""; + +// This result can't be constructed in-line, because otherwise we leak memory. +function nsAutoCompleteSimpleResult(aString) +{ + this.searchString = aString; + this.searchResult = ACR.RESULT_SUCCESS; + this.matchCount = 1; +} + +nsAutoCompleteSimpleResult.prototype = { + searchString: null, + searchResult: ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: 0, + getValueAt() { return resultText; }, + getCommentAt() { return this.getValueAt(); }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getFinalCompleteValueAt() { return this.getValueAt(); }, + getLabelAt() { return this.getValueAt(); }, + removeValueAt() {} +}; + +// A basic autocomplete implementation that returns the string contained in 'resultText'. +var autoCompleteSimpleID = Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"); +var autoCompleteSimpleName = "@mozilla.org/autocomplete/search;1?name=simple" +var autoCompleteSimple = { + QueryInterface: ChromeUtils.generateQI(["nsIFactory", "nsIAutoCompleteSearch"]), + + createInstance(iid) { + return this.QueryInterface(iid); + }, + + startSearch(aString, aParam, aResult, aListener) { + var result = new nsAutoCompleteSimpleResult(aString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {} +}; + +var componentManager = Components.manager + .QueryInterface(Ci.nsIComponentRegistrar); +componentManager.registerFactory(autoCompleteSimpleID, "Test Simple Autocomplete", + autoCompleteSimpleName, autoCompleteSimple); + +var element = document.getElementById("richautocomplete"); + +// Create stub to intercept `onSearchComplete` event. +element.onSearchComplete = function(original) { + return function() { + original.apply(this, arguments); + checkSearchCompleted(); + }; +}(element.onSearchComplete); + +SimpleTest.waitForExplicitFinish(); +setTimeout(nextTest, 0); + +/* Test cases have the following attributes: + * - search: A search string, to be emphasized in the result. + * - result: A fixed result string, so we can hardcode the expected emphasis. + * - emphasis: A list of chunks that should be emphasized or not, in strict alternation. + * - emphasizeFirst: Whether the first element of 'emphasis' should be emphasized; + * The emphasis of the other elements is defined by the strict alternation rule. + */ +let testcases = [ + { search: "test", + result: "A test string", + emphasis: ["A ", "test", " string"], + emphasizeFirst: false + }, + { search: "tea two", + result: "Tea for two, and two for tea...", + emphasis: ["Tea", " for ", "two", ", and ", "two", " for ", "tea", "..."], + emphasizeFirst: true + }, + { search: "tat", + result: "tatatat", + emphasis: ["tatatat"], + emphasizeFirst: true + }, + { search: "cheval valise", + result: "chevalise", + emphasis: ["chevalise"], + emphasizeFirst: true + } +]; +let test = -1; +let currentTest = null; + +function nextTest() { + test++; + + if (test >= testcases.length) { + // Unregister the factory so that we don't get in the way of other tests + componentManager.unregisterFactory(autoCompleteSimpleID, autoCompleteSimple); + SimpleTest.finish(); + return; + } + + // blur the field to ensure that the popup is closed and that the previous + // search has stopped, then start a new search. + let autocomplete = $("richautocomplete"); + autocomplete.blur(); + autocomplete.focus(); + + currentTest = testcases[test]; + resultText = currentTest.result; + autocomplete.value = currentTest.search; + synthesizeKey("KEY_ArrowDown"); +} + +function checkSearchCompleted() { + let autocomplete = $("richautocomplete"); + let result = autocomplete.popup.richlistbox.firstChild; + + for (let attribute of [result._titleText, result._urlText]) { + is(attribute.childNodes.length, currentTest.emphasis.length, + "The element should have the expected number of children."); + for (let i = 0; i < currentTest.emphasis.length; i++) { + let node = attribute.childNodes[i]; + // Emphasized parts strictly alternate. + if ((i % 2 == 0) == currentTest.emphasizeFirst) { + // Check that this part is correctly emphasized. + is(node.nodeName, "span", ". That child should be a span node"); + ok(node.classList.contains("ac-emphasize-text"), ". That child should be emphasized"); + is(node.textContent, currentTest.emphasis[i], ". That emphasis should be as expected."); + } else { + // Check that this part is _not_ emphasized. + is(node.nodeName, "#text", ". That child should be a text node"); + is(node.textContent, currentTest.emphasis[i], ". That text should be as expected."); + } + } + } + + setTimeout(nextTest, 0); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml new file mode 100644 index 0000000000..b49f8a1d5e --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_mac_caret.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + onload="setTimeout(keyCaretTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:input id="autocomplete" is="autocomplete-input"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function keyCaretTest() +{ + var autocomplete = $("autocomplete"); + + autocomplete.focus(); + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "no value up"); + checkKeyCaretTest("KEY_ArrowDown", 0, 0, true, "no value down"); + + autocomplete.value = "Sample"; + + autocomplete.selectionStart = 3; + autocomplete.selectionEnd = 3; + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with caret in middle"); + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with caret in middle again"); + + autocomplete.selectionStart = 2; + autocomplete.selectionEnd = 2; + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with caret in middle"); + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with caret in middle again"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("KEY_ArrowUp", 0, 0, true, "value up with selection"); + + autocomplete.selectionStart = 1; + autocomplete.selectionEnd = 4; + checkKeyCaretTest("KEY_ArrowDown", 6, 6, true, "value down with selection"); + + SimpleTest.finish(); +} + +function checkKeyCaretTest(key, expectedStart, expectedEnd, result, testid) +{ + var autocomplete = $("autocomplete"); + var keypressFired = false; + function listener(event) { + if (event.target == autocomplete) { + keypressFired = true; + } + } + SpecialPowers.addSystemEventListener(window, "keypress", listener, false); + synthesizeKey(key, {}); + SpecialPowers.removeSystemEventListener(window, "keypress", listener, false); + is(keypressFired, result, `${testid} keypress event should${result ? "" : " not"} be fired`); + is(autocomplete.selectionStart, expectedStart, testid + " selectionStart"); + is(autocomplete.selectionEnd, expectedEnd, testid + " selectionEnd"); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml new file mode 100644 index 0000000000..33fca83c32 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_placehold_last_complete.xhtml @@ -0,0 +1,303 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Autocomplete Widget Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="runTest();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + +<html:input id="autocomplete" + is="autocomplete-input" + completedefaultindex="true" + timeout="0" + autocompletesearch="simple"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function autoCompleteSimpleResult(aString, searchId) { + this.searchString = aString; + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; + this.matchCount = 1; + if (aString.startsWith('ret')) { + this._param = autoCompleteSimpleResult.retireCompletion; + } else { + this._param = "Result"; + } + this._searchId = searchId; +} +autoCompleteSimpleResult.retireCompletion = "Retire"; +autoCompleteSimpleResult.prototype = { + _param: "", + searchString: null, + searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE, + defaultIndex: 0, + errorDescription: null, + matchCount: 0, + getValueAt() { return this._param; }, + getCommentAt() { return null; }, + getStyleAt() { return null; }, + getImageAt() { return null; }, + getLabelAt() { return null; }, + removeValueAt() {} +}; + +var searchCounter = 0; + +// A basic autocomplete implementation that returns one result. +let autoCompleteSimple = { + classID: Components.ID("0a2afbdb-f30e-47d1-9cb1-0cd160240aca"), + contractID: "@mozilla.org/autocomplete/search;1?name=simple", + searchAsync: false, + pendingSearch: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIFactory", + "nsIAutoCompleteSearch" + ]), + createInstance(iid) { + return this.QueryInterface(iid); + }, + + registerFactory() { + let registrar = + Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(this.classID, "Test Simple Autocomplete", + this.contractID, this); + }, + unregisterFactory() { + let registrar = + Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(this.classID, this); + }, + + startSearch(aString, aParam, aResult, aListener) { + let result = new autoCompleteSimpleResult(aString); + + if (this.searchAsync) { + // Simulate an async search by using a timeout before invoking the + // |onSearchResult| callback. + // Store the searchTimeout such that it can be canceled if stopSearch is called. + this.pendingSearch = setTimeout(() => { + this.pendingSearch = null; + + aListener.onSearchResult(this, result); + + // Move to the next step in the async test. + asyncTest.next(); + }, 0); + } else { + aListener.onSearchResult(this, result); + } + }, + stopSearch() { + clearTimeout(this.pendingSearch); + } +}; + +SimpleTest.waitForExplicitFinish(); + +let gACTimer; +let gAutoComplete; +let asyncTest; + +let searchCompleteTimeoutId = null; + +function finishTest() { + // Unregister the factory so that we don't get in the way of other tests + autoCompleteSimple.unregisterFactory(); + SimpleTest.finish(); +} + +function runTest() { + autoCompleteSimple.registerFactory(); + gAutoComplete = $("autocomplete"); + gAutoComplete.focus(); + + // Return the search results synchronous, which also makes the completion + // happen synchronous. + autoCompleteSimple.searchAsync = false; + + sendString("r"); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + sendString("e"); + is(gAutoComplete.value, "result", "Value should be autocompleted immediately"); + + synthesizeKey("KEY_Delete"); + is(gAutoComplete.value, "re", "Deletion should not complete value"); + + synthesizeKey("KEY_Backspace"); + is(gAutoComplete.value, "r", "Backspace should not complete value"); + + synthesizeKey("KEY_ArrowLeft"); + is(gAutoComplete.value, "r", "Value should stay same when navigating with cursor"); + + runAsyncTest(); +} + +function* asyncTestGenerator() { + sendString("re"); + is(gAutoComplete.value, "re", "Value should not be autocompleted immediately"); + + // Calling |yield undefined| makes this generator function wait until + // |asyncTest.next();| is called. This happens from within the + // |autoCompleteSimple.startSearch()| function once the simulated async + // search has finished. + // Therefore, the effect of the |yield undefined;| here (and the ones) below + // is to wait until the async search result comes back. + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted"); + + // Test if typing the `s` character completes directly based on the last + // completion + sendString("s"); + is(gAutoComplete.value, "result", "Value should be completed immediately"); + + yield undefined; + + is(gAutoComplete.value, "result", "Value should be autocompleted to same value"); + synthesizeKey("KEY_Delete"); + is(gAutoComplete.value, "res", "Deletion should not complete value"); + + // No |yield undefined| needed here as no completion is triggered by the deletion. + + is(gAutoComplete.value, "res", "Still no complete value after deletion"); + + synthesizeKey("KEY_Backspace"); + is(gAutoComplete.value, "re", "Backspace should not complete value"); + + yield undefined; + + is(gAutoComplete.value, "re", "Value after search due to backspace should stay the same"); (3) + + // Typing a character that is not like the previous match. In this case, the + // completion cannot happen directly and therefore the value will be completed + // only after the search has finished. + sendString("t"); + is(gAutoComplete.value, "ret", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + sendString("i"); + is(gAutoComplete.value, "retire", "Value should be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted to the same value"); + + // Setup the scene to test how the completion behaves once the placeholder + // completion and the result from the search do not agree with each other. + gAutoComplete.value = 'r'; + // Need to type two characters as the input was reset and the autocomplete + // controller things, ther user hit the backspace button, in which case + // no completion is performed. But as a completion is desired, another + // character `t` is typed afterwards. + sendString("e"); + yield undefined; + sendString("t"); + is(gAutoComplete.value, "ret", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // The placeholder string is now set to "retire". Changing the completion + // string to "retirement" and see what the completion will turn out like. + autoCompleteSimpleResult.retireCompletion = "Retirement"; + sendString("i"); + is(gAutoComplete.value, "retire", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on search result"); + + // Change the search result to `Retire` again and see if the new result is + // complited. + autoCompleteSimpleResult.retireCompletion = "Retire"; + sendString("r"); + is(gAutoComplete.value, "retirement", "Value should be autocompleted based on placeholder"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted based on search result"); + + // Complete the value + gAutoComplete.value = 're'; + sendString("t"); + yield undefined; + sendString("i"); + is(gAutoComplete.value, "reti", "Value should not be autocompleted"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + // Remove the selected text "re" (1) and the "et" (2). Afterwards, add it again (3). + // This should not cause the completion to kick in. + synthesizeKey("KEY_Delete"); // (1) + + is(gAutoComplete.value, "reti", "Value should not complete after deletion"); + + gAutoComplete.selectionStart = 1; + gAutoComplete.selectionEnd = 3; + synthesizeKey("KEY_Delete"); // (2) + + is(gAutoComplete.value, "ri", "Value should stay unchanged after removing character in the middle"); + + yield undefined; + + sendString("e"); // (3.1) + is(gAutoComplete.value, "rei", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + sendString("t"); // (3.2) + is(gAutoComplete.value, "reti", "Inserting a character in the middle should not complete the value"); + + yield undefined; + + // Adding a new character at the end should not cause the completion to happen again + // as the completion failed before. + gAutoComplete.selectionStart = 4; + gAutoComplete.selectionEnd = 4; + sendString("r"); + is(gAutoComplete.value, "retir", "Value should not be autocompleted immediately"); + + yield undefined; + + is(gAutoComplete.value, "retire", "Value should be autocompleted"); + + finishTest(); + yield undefined; +} + +function runAsyncTest() { + gAutoComplete.value = ''; + autoCompleteSimple.searchAsync = true; + + asyncTest = asyncTestGenerator(); + asyncTest.next(); +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html new file mode 100644 index 0000000000..fbcd44e830 --- /dev/null +++ b/toolkit/content/tests/chrome/test_autocomplete_with_composition_on_input.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>autocomplete with composition tests on HTML input element</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="file_autocomplete_with_composition.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="input"><input type="submit"> + </form> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var formFillController = + SpecialPowers.getFormFillController() + .QueryInterface(Ci.nsIAutoCompleteInput); + var originalFormFillTimeout = formFillController.timeout; + + SpecialPowers.attachFormFillControllerTo(window); + var target = document.getElementById("input"); + + // Register a word to the form history. + let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { + /* eslint-env mozilla/chrome-script */ + let {FormHistory} = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" + ); + FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); + }); + chromeScript.destroy(); + target.focus(); + + new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (asynchronously search)", + window, target, formFillController.controller, is, + function() { return target.value; }, + function() { + target.setAttribute("timeout", 0); + new nsDoTestsForAutoCompleteWithComposition( + "Testing on HTML input (synchronously search)", + window, target, formFillController.controller, is, + function() { return target.value; }, + function() { + formFillController.timeout = originalFormFillTimeout; + SpecialPowers.detachFormFillControllerFrom(window); + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_browser_drop.xhtml b/toolkit/content/tests/chrome/test_browser_drop.xhtml new file mode 100644 index 0000000000..3b0f0fdb2b --- /dev/null +++ b/toolkit/content/tests/chrome/test_browser_drop.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Browser Drop Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script><![CDATA[ +SimpleTest.waitForExplicitFinish(); +function runTest() { + add_task(async function() { + let win = window.browsingContext.topChromeWindow.openDialog("window_browser_drop.xhtml", "_blank", "chrome,width=200,height=200", window); + await SimpleTest.promiseFocus(win); + for (let browserType of ["content", "remote-content"]) { + await win.dropLinksOnBrowser(win.document.getElementById(browserType + "child"), browserType); + } + await win.dropLinksOnBrowser(win.document.getElementById("chromechild"), "chrome"); + }); +} +//]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug1048178.xhtml b/toolkit/content/tests/chrome/test_bug1048178.xhtml new file mode 100644 index 0000000000..d9a34c1da3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug1048178.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1048178 +--> +<window title="Mozilla Bug 1048178" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1048178" + target="_blank">Mozilla Bug 1048178</a> + + <hbox> + <scrollbar id="scroller" + orient="horizontal" + curpos="0" + maxpos="500" + pageincrement="500" + style="width: 500px; margin:0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 1048178 **/ +var scrollbarTester = { + scrollbar: null, + startTest() { + this.scrollbar = $("scroller"); + this.setScrollToClick(false); + this.testThumbDragging(); + SimpleTest.finish(); + }, + testThumbDragging() { + var x = 400; // on the right half of the scroolbar + var y = 5; + + this.mousedown(x, y, 0); + this.mousedown(x, y, 2); + this.mouseup(x, y, 2); + this.mouseup(x, y, 0); + + var newPos = this.getPos(); // should be '500' + + this.mousedown(x, y, 0); + this.mousemove(x-1, y, 0); + this.mouseup(x-1, y, 0); + + var newPos2 = this.getPos(); + ok(newPos2 < newPos, + "Scrollbar thumb should follow the mouse when dragged."); + }, + setScrollToClick(value) { + SpecialPowers.Services.prefs.getBranch("ui.") + .setIntPref("scrollToClick", value ? 1 : 0); + }, + getPos() { + return this.scrollbar.getAttribute("curpos"); + }, + mousedown(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button }); + }, + mousemove(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button }); + }, + mouseup(x, y, button) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug263683.xhtml b/toolkit/content/tests/chrome/test_bug263683.xhtml new file mode 100644 index 0000000000..64c7df08ac --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug263683.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=263683 +--> +<window title="Mozilla Bug 263683" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=263683"> + Mozilla Bug 263683 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 263683 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug263683_window.xhtml", "263683test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug304188.xhtml b/toolkit/content/tests/chrome/test_bug304188.xhtml new file mode 100644 index 0000000000..1047528d20 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug304188.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=304188 +--> +<window title="Mozilla Bug 304188" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=304188">Mozilla Bug 304188</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 304188 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug304188_window.xhtml", "findbartest", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug331215.xhtml b/toolkit/content/tests/chrome/test_bug331215.xhtml new file mode 100644 index 0000000000..22560089c1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug331215.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=331215 +--> +<window title="Mozilla Bug 331215" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=331215">Mozilla Bug 331215</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 331215 **/ + +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug331215_window.xhtml", "331215test", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360220.xhtml b/toolkit/content/tests/chrome/test_bug360220.xhtml new file mode 100644 index 0000000000..a942e0acd6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360220.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360220 +--> +<window title="Mozilla Bug 360220" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=360220">Mozilla Bug 360220</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<menulist id="menulist"> + <menupopup> + <menuitem id="firstItem" label="foo" selected="true"/> + <menuitem id="secondItem" label="bar"/> + </menupopup> +</menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +/** Test for Bug 360220 **/ + +var menulist = document.getElementById("menulist"); +var secondItem = document.getElementById("secondItem"); +menulist.selectedItem = secondItem; + +is(menulist.label, "bar", "second item was not selected"); + +let mutObserver = new MutationObserver(() => { + is(menulist.label, "new label", "menulist label was not updated to the label of its selected item"); + done(); +}); +mutObserver.observe(menulist, { attributeFilter: ['label'] }); +secondItem.label = "new label"; + +let failureTimeout = setTimeout(function() { + ok(false, "menulist label should have updated"); + done(); +}, 2000); + +function done() { + mutObserver.disconnect(); + clearTimeout(failureTimeout); + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug360437.xhtml b/toolkit/content/tests/chrome/test_bug360437.xhtml new file mode 100644 index 0000000000..dc592e4141 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug360437.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=360437 +--> +<window title="Mozilla Bug 360437" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=360437">Mozilla Bug 360437</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 360437 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug360437_window.xhtml", "360437test", + "chrome,width=600,height=600,noopener", window); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug365773.xhtml b/toolkit/content/tests/chrome/test_bug365773.xhtml new file mode 100644 index 0000000000..e85a590608 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug365773.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=365773
+-->
+<window title="Mozilla Bug 365773"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=365773">Mozilla Bug 365773</a>
+<p id="display">
+ <radiogroup id="group" collapsed="true" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <radio id="item" label="Item"/>
+ </radiogroup>
+</p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+</pre>
+</body>
+
+<script class="testbody" type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 365773 **/
+
+function selectItem(item, isIndex, testName) {
+ var exception = null;
+ try {
+ if (isIndex)
+ document.getElementById("group").selectedIndex = item;
+ else
+ document.getElementById("group").selectedItem = item;
+ }
+ catch(e) {
+ exception = e;
+ }
+
+ ok(exception == null, testName);
+}
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = function runTests() {
+ var item = document.getElementById("item");
+
+ selectItem(item, false, "Radio button selected with selectedItem (not focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (not focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (not focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (not focused)");
+
+ document.getElementById("group").focus();
+
+ selectItem(item, false, "Radio button selected with selectedItem (focused)");
+ selectItem(null, false, "Radio button deselected with selectedItem (focused)");
+ selectItem(0, true, "Radio button selected with selectedIndex (focused)");
+ selectItem(-1, true, "Radio button deselected with selectedIndex (focused)");
+
+ SimpleTest.finish();
+};
+]]>
+</script>
+
+</window>
diff --git a/toolkit/content/tests/chrome/test_bug366992.xhtml b/toolkit/content/tests/chrome/test_bug366992.xhtml new file mode 100644 index 0000000000..9b178f1abe --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug366992.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=366992 +--> +<window title="Mozilla Bug 366992" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=366992">Mozilla Bug 366992</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 366992 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug366992_window.xhtml", "findbartest", + "chrome,width=600,height=600,noopener", window); + + + + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug382990.xhtml b/toolkit/content/tests/chrome/test_bug382990.xhtml new file mode 100644 index 0000000000..1f937ac1b6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug382990.xhtml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=382990 +--> +<window title="Mozilla Bug 382990" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="startThisTest()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=382990" + target="_blank">Mozilla Bug 382990</a> + </body> + + <tree id="testTree" height="200px"> + <treecols> + <treecol flex="1" label="Name" id="name"/> + </treecols> + <treechildren> + <treeitem><treerow><treecell label="a"/></treerow></treeitem> + <treeitem><treerow><treecell label="z"/></treerow></treeitem> + </treechildren> + </tree> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + /** Test for Bug 382990 **/ + + SimpleTest.waitForExplicitFinish(); + function startThisTest() + { + var treeElem = document.getElementById("testTree"); + treeElem.view.selection.select(0); + treeElem.focus(); + synthesizeKey("z", {ctrlKey: true}); + ok(!treeElem.view.selection.isSelected(1), "Tree selection should not change for key events with ctrl pressed."); + SimpleTest.finish(); + } + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug409624.xhtml b/toolkit/content/tests/chrome/test_bug409624.xhtml new file mode 100644 index 0000000000..6aa5f2ded9 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug409624.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=409624 +--> +<window title="Mozilla Bug 409624" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=409624"> + Mozilla Bug 409624 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 409624 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug409624_window.xhtml", "409624test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug418874.xhtml b/toolkit/content/tests/chrome/test_bug418874.xhtml new file mode 100644 index 0000000000..d91289f11b --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug418874.xhtml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <html:input id="t1" placeholder="empty"/> + </hbox> + + <hbox> + <html:input id="t2" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var t2 = $("t2"); + setTextboxValue(t1, "1"); + is(t1.editor.canUndo, true, + "undo correctly enabled when placeholder was not changed through property"); + + t2.placeholder = "reallyempty"; + setTextboxValue(t2, "2"); + is(t2.editor.canUndo, true, + "undo correctly enabled when placeholder explicitly changed through property"); + + SimpleTest.finish(); + } + + function setTextboxValue(textbox, value) { + textbox.focus(); + sendString(value); + textbox.blur(); + } + + SimpleTest.waitForFocus(doTest); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug429723.xhtml b/toolkit/content/tests/chrome/test_bug429723.xhtml new file mode 100644 index 0000000000..865f0d66f8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug429723.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429723 +--> +<window title="Mozilla Bug 429723" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429723">Mozilla Bug 429723</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 429723 **/ +SimpleTest.waitForExplicitFinish(); +window.openDialog("bug429723_window.xhtml", "429723test", + "chrome,width=600,height=600,noopener", window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug451540.xhtml b/toolkit/content/tests/chrome/test_bug451540.xhtml new file mode 100644 index 0000000000..debcadfe05 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug451540.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=451540 +--> +<window title="Mozilla Bug 451540" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=451540"> + Mozilla Bug 451540 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 451540 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("bug451540_window.xhtml", "451540test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug457632.xhtml b/toolkit/content/tests/chrome/test_bug457632.xhtml new file mode 100644 index 0000000000..12267c1dec --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug457632.xhtml @@ -0,0 +1,160 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 457632 + --> +<window title="Bug 457632" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <vbox id="nb"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +function completeAnimation(nextTest) { + if (!gNotificationBox._animating) { + nextTest(); + return; + } + + setTimeout(completeAnimation, 50, nextTest); +} + +async function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + }); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + await gNotificationBox.appendNotification("notification1", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + is(gNotificationBox.allNotifications.length, 1, "Notification exists while animating in"); + let notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(notification, "Notification should exist while animating in"); + + // Wait for the notificaton to finish displaying + completeAnimation(test1); +} + +// Tests that a notification that is fully animated in gets removed immediately +async function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification1"); + ok(!notification, "Test 1 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 1 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + // Wait for the notificaton to finish hiding + completeAnimation(test2); +} + +// Tests that a notification that is animating in gets removed immediately +async function test2() { + let notification = await gNotificationBox.appendNotification("notification2", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification2"); + ok(!notification, "Test 2 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 2 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 2 should show no notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test3(); +} + +// Tests that a background notification goes away immediately +async function test3() { + let notification = await gNotificationBox.appendNotification("notification3", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + let notification2 = await gNotificationBox.appendNotification("notification4", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + is(gNotificationBox.allNotifications.length, 2, "Test 3 should show 2 notifications present"); + gNotificationBox.removeNotification(notification); + is(gNotificationBox.allNotifications.length, 1, "Test 3 should show 1 notifications present"); + notification = gNotificationBox.getNotificationWithValue("notification3"); + ok(!notification, "Test 3 showed notification was still present"); + gNotificationBox.removeNotification(notification2); + is(gNotificationBox.allNotifications.length, 0, "Test 3 should show 0 notifications present"); + notification2 = gNotificationBox.getNotificationWithValue("notification4"); + ok(!notification2, "Test 3 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 3 said there was still a current notification"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test4(); +} + +// Tests that a foreground notification hiding a background one goes away +async function test4() { + let notification = await gNotificationBox.appendNotification("notification5", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + let notification2 = await gNotificationBox.appendNotification("notification6", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + gNotificationBox.removeNotification(notification2); + notification2 = gNotificationBox.getNotificationWithValue("notification6"); + ok(!notification2, "Test 4 showed notification2 was still present"); + is(gNotificationBox.currentNotification, notification, "Test 4 said the current notification was wrong"); + is(gNotificationBox.allNotifications.length, 1, "Test 4 should show 1 notifications present"); + gNotificationBox.removeNotification(notification); + notification = gNotificationBox.getNotificationWithValue("notification5"); + ok(!notification, "Test 4 showed notification was still present"); + ok(!gNotificationBox.currentNotification, "Test 4 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 4 should show 0 notifications present"); + + // Get rid of the hiding notifications + gNotificationBox.removeAllNotifications(true); + test5(); +} + +// Tests that removeAllNotifications gets rid of everything +async function test5() { + let notification = await gNotificationBox.appendNotification("notification7", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + let notification2 = await gNotificationBox.appendNotification("notification8", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + gNotificationBox.removeAllNotifications(); + notification = gNotificationBox.getNotificationWithValue("notification7"); + notification2 = gNotificationBox.getNotificationWithValue("notification8"); + ok(!notification, "Test 5 showed notification was still present"); + ok(!notification2, "Test 5 showed notification2 was still present"); + ok(!gNotificationBox.currentNotification, "Test 5 said there was still a current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 5 should show 0 notifications present"); + + await gNotificationBox.appendNotification("notification9", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + + // Wait for the notificaton to finish displaying + completeAnimation(test6); +} + +// Tests whether removing an already removed notification doesn't break things +async function test6() { + let notification = gNotificationBox.getNotificationWithValue("notification9"); + ok(notification, "Test 6 should have an initial notification"); + gNotificationBox.removeNotification(notification); + gNotificationBox.removeNotification(notification); + + ok(!gNotificationBox.currentNotification, "Test 6 shouldn't be any current notification"); + is(gNotificationBox.allNotifications.length, 0, "Test 6 allNotifications.length should be 0"); + notification = await gNotificationBox.appendNotification("notification10", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + is(notification, gNotificationBox.currentNotification, "Test 6 should have made the current notification"); + gNotificationBox.removeNotification(notification); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug460942.xhtml b/toolkit/content/tests/chrome/test_bug460942.xhtml new file mode 100644 index 0000000000..53f33302be --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug460942.xhtml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460942 +--> +<window title="Mozilla Bug 460942" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=460942" + target="_blank">Mozilla Bug 460942</a> + </body> + + <!-- test code goes here --> + + <richlistbox> + <richlistitem id="item1"> + <label value="one"/> + <box> + <label value="two"/> + </box> + </richlistitem> + <richlistitem id="item2"><description>one</description><description>two</description></richlistitem> + </richlistbox> + + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 460942 **/ + function runTests() { + is ($("item1").label, "one two"); + is ($("item2").label, ""); + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug471776.xhtml b/toolkit/content/tests/chrome/test_bug471776.xhtml new file mode 100644 index 0000000000..dcd45bfd77 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug471776.xhtml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Textbox with placeholder undo test" width="500" height="600" + onload="doTest();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <html:input id="t1" placeholder="empty"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + t1.focus(); + ok(!t1.editor.canUndo, "undo correctly disabled when no user edits"); + SimpleTest.finish(); + } + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug509732.xhtml b/toolkit/content/tests/chrome/test_bug509732.xhtml new file mode 100644 index 0000000000..7e340322b2 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug509732.xhtml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 509732 + --> +<window title="Bug 509732" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <vbox id="nb" hidden="true"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;" + onload="test()"/> + + <!-- test code goes here --> +<script type="application/javascript"> +<![CDATA[ +var gNotificationBox; + +// Tests that a notification that is added in an hidden box didn't throw the animation +async function test() { + SimpleTest.waitForExplicitFinish(); + gNotificationBox = new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + }); + + is(gNotificationBox.allNotifications.length, 0, "There should be no initial notifications"); + + await gNotificationBox.appendNotification("notification1", + { label: "Test notification", priority: gNotificationBox.PRIORITY_INFO_LOW }); + + is(gNotificationBox.allNotifications.length, 1, "Notification exists"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + + test1(); +} + +// Tests that a notification that is removed from an hidden box didn't throw the animation +function test1() { + let notification = gNotificationBox.getNotificationWithValue("notification1"); + gNotificationBox.removeNotification(notification); + ok(!gNotificationBox.currentNotification, "Test 1 should show no current animation"); + is(gNotificationBox._animating, false, "Notification shouldn't be animating"); + is(gNotificationBox.allNotifications.length, 0, "Test 1 should show no notifications present"); + + SimpleTest.finish(); +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_bug557987.xhtml b/toolkit/content/tests/chrome/test_bug557987.xhtml new file mode 100644 index 0000000000..6af1b13700 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug557987.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 557987 + --> +<window title="Bug 557987" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <toolbarbutton id="button" type="menu" label="Test bug 557987" + onclick="eventReceived('click');" + oncommand="eventReceived('command');"> + <menupopup onpopupshowing="eventReceived('popupshowing'); return false;" /> + </toolbarbutton> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu"/> +// This used to test menu buttons, and was updated when this button type was removed. +function test() { + disableNonTestMouseEvents(true); + + let button = $("button"); + let rightEdge = button.getBoundingClientRect().width - 2; + let centerX = button.getBoundingClientRect().width / 2; + let centerY = button.getBoundingClientRect().height / 2; + + synthesizeMouse(button, rightEdge, centerY, {}, window); + synthesizeMouse(button, centerX, centerY, {}, window); + + synthesizeMouse(document.getElementsByTagName("body")[0], 0, 0, {}, window); + + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + +} + +function finishTest() { + is(eventCount.command, 0, "Correct number of command events received"); + is(eventCount.popupshowing, 2, "Correct number of popupshowing events received"); + is(eventCount.click, 2, "Correct number of click events received"); + + SimpleTest.finish(); +} + +let eventCount = { + command: 0, + popupshowing: 0, + click: 0, +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug562554.xhtml b/toolkit/content/tests/chrome/test_bug562554.xhtml new file mode 100644 index 0000000000..a822bf59dd --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug562554.xhtml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for bug 562554 + --> +<window title="Bug 562554" width="400" height="400" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <toolbarbutton type="menu" id="toolbarmenu" style="height: 200px; justify-content: flex-start; align-items: flex-start"> + <menupopup id="menupopup" onpopupshowing="eventReceived('popupshowing'); return false;"/> + <stack style="pointer-events: none"> + <button style="pointer-events: auto; width: 100px; height: 30px; margin-left: 0; margin-top: 0;" allowevents="true" + onclick="eventReceived('clickbutton1'); return false;"/> + <button style="width: 100px; height: 30px; margin-left: 70px; margin-top: 0;" + onclick="eventReceived('clickbutton2'); return false;"/> + </stack> + </toolbarbutton> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test); + +// Tests that mouse events are correctly dispatched to <toolbarbutton type="menu"/> +function test() { + disableNonTestMouseEvents(true); + nextTest(); +} + +let tests = [ + // Click on the toolbarbutton itself - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 10, 50, {}, window), + () => is(eventCount.popupshowing, 1, "Got first popupshowing event"), + + // Click on button1 which has allowevents="true" - should call clickbutton1 + () => synthesizeMouse($("toolbarmenu"), 10, 15, {}, window), + () => is(eventCount.clickbutton1, 1, "Button 1 clicked"), + + // Click on button2 outside of intersection - should call popupshowing + () => synthesizeMouse($("toolbarmenu"), 150, 15, {}, window) +]; + +function nextTest() { + if (tests.length) { + let func = tests.shift(); + func(); + SimpleTest.executeSoon(nextTest); + } else { + disableNonTestMouseEvents(false); + SimpleTest.executeSoon(finishTest); + } +} + +function finishTest() { + is(eventCount.clickbutton1, 1, "Correct number of clicks on button 1"); + is(eventCount.clickbutton2, 0, "Correct number of clicks on button 2"); + is(eventCount.popupshowing, 2, "Correct number of popupshowing events received"); + + SimpleTest.finish(); +} + +let eventCount = { + popupshowing: 0, + clickbutton1: 0, + clickbutton2: 0 +}; + +function eventReceived(eventName) { + eventCount[eventName]++; +} + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug624329.xhtml b/toolkit/content/tests/chrome/test_bug624329.xhtml new file mode 100644 index 0000000000..26b7d2ea53 --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug624329.xhtml @@ -0,0 +1,169 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=624329 +--> +<window title="Mozilla Bug 624329 context menu position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" + onload="beginTest()"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=624329" + target="_blank">Mozilla Bug 624329</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 624329 **/ + +SimpleTest.waitForExplicitFinish(); + +var win; +var timeoutID; +var menu; + +const {AppConstants} = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +function beginTest() { + if (AppConstants.platform == "macosx" && SpecialPowers.getBoolPref("widget.macos.native-context-menus", false)) { + // This test does not apply with native context menus. + ok(true, "macOS positions native menus, so we don't need to test this behaviour."); + SimpleTest.finish(); + return; + } + + openTestWindow(); +} + +function openTestWindow() { + win = window.browsingContext.topChromeWindow.openDialog("bug624329_window.xhtml", "_blank", "width=300,resizable=yes,chrome", window); + // Close our window if the test times out so that it doesn't interfere + // with later tests. + timeoutID = setTimeout(function () { + ok(false, "Test timed out."); + // Provide some time for a screenshot + setTimeout(finish, 1000); + }, 20000); +} + +function listenOnce(event, callback) { + win.addEventListener(event, function listener() { + callback(); + }, { once: true}); +} + +function childFocused() { + // maximizing the window is a simple way to ensure that the menu is near + // the right edge of the screen. + + listenOnce("resize", childResized); + win.maximize(); +} + +function childResized() { + const isOSXMavericks = navigator.userAgent.includes("Mac OS X 10.9"); + const isOSXYosemite = navigator.userAgent.includes("Mac OS X 10.10"); + if (isOSXMavericks || isOSXYosemite) { + todo_is(win.windowState, win.STATE_MAXIMIZED, + "A resize before being maximized breaks this test on 10.9 and 10.10"); + finish(); + return; + } + + is(win.windowState, win.STATE_MAXIMIZED, + "window should be maximized"); + + isnot(win.innerWidth, 300, + "window inner width should have changed"); + + openContextMenu(); +} + +function openContextMenu() { + var mouseX = win.innerWidth - 10; + var mouseY = 10; + + menu = win.document.getElementById("menu"); + var screenX = menu.screenX; + var screenY = menu.screenY; + var utils = win.windowUtils; + + utils.sendMouseEvent("contextmenu", mouseX, mouseY, 2, 0, 0); + + var interval = setInterval(checkMoved, 200); + function checkMoved() { + if (menu.screenX != screenX || + menu.screenY != screenY) { + clearInterval(interval); + // Wait further to check that the window does not move again. + setTimeout(checkPosition, 1000); + } + } + + function checkPosition() { + var rootElement = win.document.documentElement; + var platformIsMac = navigator.userAgent.indexOf("Mac") > -1; + + var x = menu.screenX - rootElement.screenX - parseFloat(getComputedStyle(menu).marginLeft); + var y = menu.screenY - rootElement.screenY - parseFloat(getComputedStyle(menu).marginTop); + + if (platformIsMac) + { + // This check is alterered slightly for OSX which adds padding to the top + // and bottom of its context menus. The menu position calculation must + // be changed to allow for the pointer to be outside this padding + // when the menu opens. + // (Bug 1075089) + ok(y + 6 >= mouseY, + "menu top " + (y + 6) + " should be below click point " + mouseY); + } + else + { + ok(y >= mouseY, + "menu top " + y + " should be below click point " + mouseY); + } + + ok(y <= mouseY + 20, + "menu top " + y + " should not be too far below click point " + mouseY); + + ok(x < mouseX, + "menu left " + x + " should be left of click point " + mouseX); + var right = x + menu.getBoundingClientRect().width; + + if (platformIsMac) { + // Rather than be constrained by the right hand screen edge, OSX menus flip + // horizontally and appear to the left of the mouse pointer + ok(right < mouseX, + "menu right " + right + " should be left of click point " + mouseX); + } + else { + ok(right > mouseX, + "menu right " + right + " should be right of click point " + mouseX); + } + + clearTimeout(timeoutID); + finish(); + } + +} + +function finish() { + if (menu && navigator.platform.includes("Win")) { + todo(false, "Should not have to hide popup before closing its window"); + // This avoids mochitest "Unable to restore focus" errors (bug 670053). + menu.hidePopup(); + } + win.close(); + SimpleTest.finish(); +} + + ]]> + </script> +</window> diff --git a/toolkit/content/tests/chrome/test_bug792324.xhtml b/toolkit/content/tests/chrome/test_bug792324.xhtml new file mode 100644 index 0000000000..ddedf5907b --- /dev/null +++ b/toolkit/content/tests/chrome/test_bug792324.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=792324 +--> +<window title="Mozilla Bug 792324" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=792324">Mozilla Bug 792324</a> + + <p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<panel id="panel-1"> + <button label="just a normal button"/> + <button id="button-1" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 1" + /> +</panel> + +<panel id="panel-2"> + <button label="just a normal button"/> + <button id="button-2" + accesskey="X" + oncommand="clicked(event)" + label="Button in panel 2" + /> +</panel> + +<script class="testbody" type="application/javascript"><![CDATA[ + +/** Test for Bug 792324 **/ +let after_click; + +function clicked(event) { + after_click(event); +} + +function checkAccessKeyOnPanel(panelid, buttonid, cb) { + let panel = document.getElementById(panelid); + panel.addEventListener("popupshown", function onpopupshown() { + panel.firstChild.focus(); + after_click = function(event) { + is(event.target.id, buttonid, "Accesskey was directed to the button '" + buttonid + "'"); + panel.hidePopup(); + cb(); + } + sendString("X"); + }); + panel.openPopup(null, "", 100, 100, false, false); +} + +function test() { + checkAccessKeyOnPanel("panel-1", "button-1", function() { + checkAccessKeyOnPanel("panel-2", "button-2", function() { + SimpleTest.finish(); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test, window); + +]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_button.xhtml b/toolkit/content/tests/chrome/test_button.xhtml new file mode 100644 index 0000000000..fa1b7cacd0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_button.xhtml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for button + --> +<window title="Button Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="one" label="One" /> +<button id="two" label="Two"/> +<hbox> + <button id="three" label="Three" open="true"/> +</hbox> +<hbox> + <button id="four" type="menu" label="Four"/> + <button id="five" label="Five"/> +</hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +add_task(async function test_button() +{ + await SimpleTest.promiseFocus(); + + // Click on the button. + let commandPromise = new Promise(resolve => { + addEventListener("command", event => resolve(event), { once: true }); + }); + + synthesizeMouseAtCenter($("one"), {}); + let event = await commandPromise; + is(event.button, 0, "button for mouse"); + is(event.inputSource, MouseEvent.MOZ_SOURCE_MOUSE, "input source for mouse"); + + // Press space while to button is focused. + commandPromise = new Promise(resolve => { + addEventListener("command", event => resolve(event), { once: true }); + }); + + $("one").focus(); + synthesizeKey("VK_SPACE", { }); + event = await commandPromise; + is(event.button, 0, "button for keyboard"); + is(event.inputSource, MouseEvent.MOZ_SOURCE_KEYBOARD, "input source for keyboard"); + + $("two").disabled = true; + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "!command", "button press command when disabled"); + synthesizeMouseExpectEvent($("two"), 2, 2, {}, $("two"), "click", "button press click when disabled"); + + if (!navigator.platform.includes("Mac")) { + $("one").focus(); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, $("three"), "key cursor down on button"); + + synthesizeKey("KEY_ArrowRight"); + is(document.activeElement, $("four"), "key cursor right on button"); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, $("four"), "key cursor down on menu button"); + + $("three").focus(); + synthesizeKey("KEY_ArrowUp"); + is(document.activeElement, $("one"), "key cursor up on button"); + } + + $("two").focus(); + ok(document.activeElement != $("two"), "focus disabled button"); +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_chromemargin.xhtml b/toolkit/content/tests/chrome/test_chromemargin.xhtml new file mode 100644 index 0000000000..d1a6a568be --- /dev/null +++ b/toolkit/content/tests/chrome/test_chromemargin.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom chrome margin tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Tests parsing of the chrome margin attrib on a window. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_chromemargin.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml b/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml new file mode 100644 index 0000000000..7b29bd6c5d --- /dev/null +++ b/toolkit/content/tests/chrome/test_closemenu_attribute.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu closemenu Attribute Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="menu" type="menu" label="Menu" onpopuphidden="popupHidden(event)"> + <menupopup id="p1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l1" label="One"> + <menupopup id="p2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="l2" label="Two"> + <menupopup id="p3" onpopupshown="executeMenuItem()"> + <menuitem id="l3" label="Three"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedId = "p3"; +var gMode = -1; +var gModes = ["", "auto", "single", "none"]; + +function nextTest() +{ + gMode++; + if (gModes[gMode] != "none") + gExpectedId = "p3"; + + if (gMode != 0) + $("l3").setAttribute("closemenu", gModes[gMode]); + if (gModes[gMode] == "none") + $("l2").open = true; + else + $("menu").open = true; +} + +function executeMenuItem() +{ + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + // after a couple of seconds, end the test, as the 'none' closemenu value + // should not hide any popups + if (gModes[gMode] == "none") + setTimeout(function() { $("menu").open = false; }, 2000); +} + +function popupHidden(event) +{ + if (gModes[gMode] == "none") { + if (event.target.id == "p1") + SimpleTest.finish() + return; + } + + is(event.target.id, gExpectedId, + "Expected event " + gModes[gMode] + " " + gExpectedId); + + gExpectedId = ""; + if (event.target.id == "p3") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p2"; + } + else if (event.target.id == "p2") { + if (gModes[gMode] == "" || gModes[gMode] == "auto") + gExpectedId = "p1"; + } + + if (!gExpectedId) + nextTest(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_contextmenu_list.xhtml b/toolkit/content/tests/chrome/test_contextmenu_list.xhtml new file mode 100644 index 0000000000..d5d2b1a10b --- /dev/null +++ b/toolkit/content/tests/chrome/test_contextmenu_list.xhtml @@ -0,0 +1,296 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context Menu on List Tests" + onload="setTimeout(startTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<spacer style="height: 5px"/> + +<hbox style="padding-left: 10px;"> + <spacer style="width: 5ps"/> + <richlistbox id="list" context="themenu" style="padding: 0;" oncontextmenu="checkContextMenu(event)"> + <richlistitem id="item1" style="padding-top: 4px; margin: 0;"><button label="One"/></richlistitem> + <richlistitem id="item2" style="height: 22px"><checkbox label="Checkbox"/></richlistitem> + <richlistitem id="item3"><button label="Three"/></richlistitem> + <richlistitem id="item4"><checkbox label="Four"/></richlistitem> + </richlistbox> + + <tree id="tree" rows="5" flex="1" context="themenu" style="-moz-appearance: none; border: 0"> + <treecols> + <treecol label="Name" flex="1"/> + <splitter class="tree-splitter"/> + <treecol label="Moons"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Mercury"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Venus"/> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Earth"/> + <treecell label="1"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mars"/> + <treecell label="2"/> + </treerow> + </treeitem> + </treechildren> + </tree> + + <menu id="menu" label="Menu"> + <menupopup id="menupopup" onpopupshown="menuTests()" onpopuphidden="nextTest()" + oncontextmenu="checkContextMenuForMenu(event)"> + <menuitem id="menu1" label="Menu 1"/> + <menuitem id="menu2" label="Menu 2"/> + <menuitem id="menu3" label="Menu 3"/> + </menupopup> + </menu> + +</hbox> + +<menupopup id="themenu" onpopupshowing="if (gTestId == -1) event.preventDefault()" + onpopupshown="checkPopup()" onpopuphidden="setTimeout(nextTest, 0);"> + <menuitem label="Item"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gTestId = -1; +var gTestElement = "list"; +var gSelectionStep = 0; +var gContextMenuFired = false; + +async function startTest() +{ + // These tests check behavior of non-native menus, and use anchored and non-anchored popups + // somewhat interchangeably. So disable native context menus for this test. + // We will have separate tests for native context menu behavior, see bug 1700727. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + + // first, check if the richlistbox selection changes on a contextmenu mouse event + var element = $("list"); + synthesizeMouse(element.getItemAtIndex(3), 7, 1, { type : "mousedown", button: 2, ctrlKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2, ctrlKey: true, shiftKey: true }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + gSelectionStep++; + synthesizeMouse(element.getItemAtIndex(1), 7, 1, { type : "mousedown", button: 2 }); + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + + $("menu").open = true; +} + +function menuTests() +{ + gSelectionStep = 0; + var element = $("menu"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + is(gContextMenuFired, true, "context menu fired when menu open"); + + gSelectionStep = 1; + $("menu").activeChild = $("menu2"); + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + + $("menu").open = false; +} + +function nextTest() +{ + gTestId++; + if (gTestId > 2) { + if (gTestElement == "list") { + gTestElement = "tree"; + gTestId = 0; + } + else { + SimpleTest.finish(); + return; + } + } + var element = $(gTestElement); + element.focus(); + if (gTestId == 0) { + if (gTestElement == "list") + element.selectedIndex = 2; + element.currentIndex = 2; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } + else if (gTestId == 1) { + synthesizeMouse(element, 7, 4, { type : "contextmenu", button: 2 }); + } + else { + element.currentIndex = -1; + element.selectedIndex = -1; + synthesizeMouse(element, 0, 0, { type : "contextmenu", button: 0 }); + } +} + +// This is nasty so I'd better explain what's going on. +// The basic problem is that the synthetic mouse coordinate generated +// by DOMWindowUtils.sendMouseEvent and also the synthetic mouse coordinate +// generated internally when contextmenu events are redirected to the focused +// element are rounded to the nearest device pixel. But this rounding is done +// while the coordinates are relative to the nearest widget. When this test +// is run in the mochitest harness, the nearest widget is the main mochitest +// window, and our document can have a fractional position within that +// mochitest window. So when we round coordinates for comparison in this +// test, we need to do so very carefully, especially if the target element +// also has a fractional position within our document. +// +// For example, if the y-offset of our containing IFRAME is 100.4px, +// and the offset of our expected point is 10.3px in our document, the actual +// mouse event is dispatched to round(110.7) == 111px. This comes back +// with a clientY of round(111 - 100.4) == round(10.6) == 11. This is not +// equal to round(10.3) as you might expect. + +function isRoundedX(a, b, msg) +{ + is(Math.round(a + window.mozInnerScreenX), Math.round(b + window.mozInnerScreenX), msg); +} + +function isRoundedY(a, b, msg) +{ + is(Math.round(a + window.mozInnerScreenY), Math.round(b + window.mozInnerScreenY), msg); +} + +function checkContextMenu(event) +{ + var rect = $(gTestElement).getBoundingClientRect(); + + var frombase = (gTestId == -1 || gTestId == 1); + if (!frombase) + rect = event.originalTarget.getBoundingClientRect(); + var left = frombase ? rect.left + 7 : rect.left; + var top = frombase ? rect.top + 4 : rect.bottom; + + isRoundedX(event.clientX, left, gTestElement + " clientX " + gSelectionStep + " " + gTestId + "," + frombase); + isRoundedY(event.clientY, top, gTestElement + " clientY " + gSelectionStep + " " + gTestId); + ok(event.screenX > left, gTestElement + " screenX " + gSelectionStep + " " + gTestId); + ok(event.screenY > top, gTestElement + " screenY " + gSelectionStep + " " + gTestId); + + // context menu from mouse click + switch (gTestId) { + case -1: + // eslint-disable-next-line no-nested-ternary + var expected = gSelectionStep == 2 ? 1 : (platformIsMac() ? 3 : 0); + is($(gTestElement).selectedIndex, expected, "index after click " + gSelectionStep); + break; + case 0: + if (gTestElement == "list") + is(event.originalTarget, $("item3"), "list selection target"); + else + is(event.originalTarget, $("treechildren"), "tree selection target"); + break; + case 1: + is(event.originalTarget.id, $("item1").id, "list mouse selection target"); + break; + case 2: + is(event.originalTarget, $("list"), "list no selection target"); + break; + } +} + +function checkContextMenuForMenu(event) +{ + gContextMenuFired = true; + + var popuprect = (gSelectionStep ? $("menu2") : $("menupopup")).getBoundingClientRect(); + is(event.clientX, Math.round(popuprect.left), "menu left " + gSelectionStep); + // the clientY is off by one sometimes on Windows (when loaded in the testing iframe + // but not when loaded separately) so just check for both cases for now + ok(event.clientY == Math.round(popuprect.bottom) || + event.clientY - 1 == Math.round(popuprect.bottom), "menu top " + gSelectionStep); +} + +function checkPopup() +{ + var menurect = $("themenu").getBoundingClientRect(); + + // Context menus are offset by a number of pixels from the mouse click + // which activates them. This is so that they don't appear exactly + // under the mouse which can cause them to be mistakenly dismissed. + // The number of pixels depends on the platform and is defined in + // each platform's nsLookAndFeel + var contextMenuOffsetX = platformIsMac() ? 1 : 2; + var contextMenuOffsetY = platformIsMac() ? -6 : 2; + contextMenuOffsetY += parseFloat(getComputedStyle($("themenu")).marginTop); + contextMenuOffsetX += parseFloat(getComputedStyle($("themenu")).marginLeft); + + if (gTestId == 0) { + if (gTestElement == "list") { + var itemrect = $("item3").getBoundingClientRect(); + isRoundedX(menurect.left, itemrect.left + contextMenuOffsetX, + "list selection keyboard left"); + isRoundedY(menurect.top, itemrect.bottom + contextMenuOffsetY, + "list selection keyboard top"); + } + else { + var tree = $("tree"); + var bodyrect = $("treechildren").getBoundingClientRect(); + isRoundedX(menurect.left, bodyrect.left + contextMenuOffsetX, + "tree selection keyboard left"); + isRoundedY(menurect.top, bodyrect.top + + tree.rowHeight * 3 + contextMenuOffsetY, + "tree selection keyboard top"); + } + } + else if (gTestId == 1) { + // activating a context menu with the mouse from position (7, 4). + let elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + 7 + contextMenuOffsetX, + gTestElement + " mouse left"); + isRoundedY(menurect.top, elementrect.top + 4 + contextMenuOffsetY, + gTestElement + " mouse top"); + } + else { + let elementrect = $(gTestElement).getBoundingClientRect(); + isRoundedX(menurect.left, elementrect.left + contextMenuOffsetX, + gTestElement + " no selection keyboard left"); + isRoundedY(menurect.top, elementrect.bottom + contextMenuOffsetY, + gTestElement + " no selection keyboard top"); + } + + $("themenu").hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_contextmenu_rtl.xhtml b/toolkit/content/tests/chrome/test_contextmenu_rtl.xhtml new file mode 100644 index 0000000000..45649c8c2d --- /dev/null +++ b/toolkit/content/tests/chrome/test_contextmenu_rtl.xhtml @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Context Menu RTL position" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<menupopup id="context-menu" style="max-width: 100px; direction: rtl"> + <menuitem label="Item"/> +</menupopup> +<script> +<![CDATA[ + +add_task(async function() { + // This test checks behavior of non-native menus, + // so disable native context menus for this test. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + const menu = document.getElementById("context-menu"); + const shown = new Promise(resolve => menu.addEventListener("popupshown", resolve, { once: true })); + const point = { + x: window.screenX + screen.width / 2, + y: window.screenY + screen.height / 2, + }; + menu.openPopupAtScreen(point.x, point.y, true, null); + await shown; + const rect = menu.getBoundingClientRect(); + const margin = parseFloat(getComputedStyle(menu).marginRight); + info(`${menu.screenX} + ${rect.width} + ${margin} < ${point.x}`); + ok(menu.screenX + rect.width + margin < point.x, "Should be right-aligned"); + menu.hidePopup(); +}); + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_cursorsnap.xhtml b/toolkit/content/tests/chrome/test_cursorsnap.xhtml new file mode 100644 index 0000000000..170e0ab378 --- /dev/null +++ b/toolkit/content/tests/chrome/test_cursorsnap.xhtml @@ -0,0 +1,123 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window title="Cursor snapping test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +const kMaxRetryCount = 4; +const kTimeoutTime = [ + 100, 100, 1000, 1000, 5000 +]; + +var gRetryCount; + +var gTestingCount = 0; +var gTestingIndex = -1; +var gDisable = false; +var gHidden = false; + +function canRetryTest() +{ + return gRetryCount <= kMaxRetryCount; +} + +function getTimeoutTime() +{ + return kTimeoutTime[gRetryCount]; +} + +function runNextTest() +{ + gRetryCount = 0; + gTestingIndex++; + runCurrentTest(); +} + +function retryCurrentTest() +{ + ok(canRetryTest(), "retry the current test..."); + gRetryCount++; + runCurrentTest(); +} + +function runCurrentTest() +{ + var position = "top=" + gTestingCount + ",left=" + gTestingCount + ","; + gTestingCount++; + switch (gTestingIndex) { + case 0: + gDisable = false; + gHidden = false; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 1: + gDisable = true; + gHidden = false; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 2: + gDisable = false; + gHidden = true; + window.openDialog("window_cursorsnap_dialog.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 3: + gDisable = false; + gHidden = false; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 4: + gDisable = true; + gHidden = false; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + case 5: + gDisable = false; + gHidden = true; + window.openDialog("window_cursorsnap_wizard.xhtml", "_blank", + position + "chrome,width=100,height=100,noopener", window); + break; + default: + SetPrefs(false); + SimpleTest.finish(); + } +} + +function SetPrefs(aSet) +{ + var prefSvc = SpecialPowers.Services.prefs; + const kPrefName = "ui.cursor_snapping.always_enabled"; + if (aSet) { + prefSvc.setBoolPref(kPrefName, true); + } else if (prefSvc.prefHasUserValue(kPrefName)) { + prefSvc.clearUserPref(kPrefName); + } +} + +SetPrefs(true); +runNextTest(); + +]]> +</script> +</window> diff --git a/toolkit/content/tests/chrome/test_custom_element_base.xhtml b/toolkit/content/tests/chrome/test_custom_element_base.xhtml new file mode 100644 index 0000000000..77cc8819a3 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_base.xhtml @@ -0,0 +1,363 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom Element Base Class Tests" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <button id="one"/> + <simpleelement id="two" style="-moz-user-focus: normal;"/> + <simpleelement id="three" disabled="true" style="-moz-user-focus: normal;"/> + <button id="four"/> + <inherited-element-declarative foo="fuagra" empty-string=""></inherited-element-declarative> + <inherited-element-derived foo="fuagra"></inherited-element-derived> + <inherited-element-shadowdom-declarative foo="fuagra" empty-string=""></inherited-element-shadowdom-declarative> + <inherited-element-imperative foo="fuagra" empty-string=""></inherited-element-imperative> + <inherited-element-beforedomloaded foo="fuagra" empty-string=""></inherited-element-beforedomloaded> + + <!-- test code running before page load goes here --> + <script type="application/javascript"><![CDATA[ + class InheritAttrsChangeBeforDOMLoaded extends MozXULElement { + static get inheritedAttributes() { + return { + "label": "foo", + }; + } + connectedCallback() { + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + + this.initializeAttributeInheritance(); + is(this.label.getAttribute("foo"), "fuagra", + "InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #1"); + + this.setAttribute("foo", "chuk&gek"); + is(this.label.getAttribute("foo"), "chuk&gek", + "InheritAttrsChangeBeforDOMLoaded: attribute should be propagated #2"); + } + } + customElements.define("inherited-element-beforedomloaded", + InheritAttrsChangeBeforDOMLoaded); + ]]> + </script> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + ok(MozXULElement, "MozXULElement defined on the window"); + testMixin(); + testBaseControl(); + testBaseControlMixin(); + testBaseText(); + testParseXULToFragment(); + testInheritAttributes(); + await testCustomInterface(); + + let htmlWin = await new Promise(resolve => { + let htmlIframe = document.createXULElement("iframe"); + htmlIframe.src = "file_empty.xhtml"; + htmlIframe.onload = () => resolve(htmlIframe.contentWindow); + document.documentElement.appendChild(htmlIframe); + }); + + ok(htmlWin.MozXULElement, "MozXULElement defined on a chrome HTML window"); + SimpleTest.finish(); + } + + function testMixin() { + ok(MozElements.MozElementMixin, "Mixin exists"); + let MixedHTMLElement = MozElements.MozElementMixin(HTMLElement); + ok(MixedHTMLElement.insertFTLIfNeeded, "Mixed in class contains helper functions"); + } + + function testBaseControl() { + ok(MozElements.BaseControl, "BaseControl exists"); + ok("disabled" in MozElements.BaseControl.prototype, + "BaseControl prototype contains base control attributes"); + } + + function testBaseControlMixin() { + ok(MozElements.BaseControlMixin, "Mixin exists"); + let MixedHTMLSpanElement = MozElements.MozElementMixin(HTMLSpanElement); + let HTMLSpanBaseControl = MozElements.BaseControlMixin(MixedHTMLSpanElement); + ok("disabled" in HTMLSpanBaseControl.prototype, "Mixed in class prototype contains base control attributes"); + } + + function testBaseText() { + ok(MozElements.BaseText, "BaseText exists"); + ok("label" in MozElements.BaseText.prototype, + "BaseText prototype inherits BaseText attributes"); + ok("disabled" in MozElements.BaseText.prototype, + "BaseText prototype inherits BaseControl attributes"); + } + + function testParseXULToFragment() { + ok(MozXULElement.parseXULToFragment, "parseXULToFragment helper exists"); + + let frag = MozXULElement.parseXULToFragment(`<deck id='foo' />`); + ok(DocumentFragment.isInstance(frag)); + + document.documentElement.appendChild(frag); + + let deck = document.documentElement.lastChild; + ok(deck instanceof MozXULElement, "instance of MozXULElement"); + ok(XULElement.isInstance(deck), "instance of XULElement"); + is(deck.id, "foo", "attribute set"); + is(deck.selectedIndex, 0, "Custom Element is property attached"); + deck.remove(); + + info("Checking that whitespace text is removed but non-whitespace text isn't"); + let boxWithWhitespaceText = MozXULElement.parseXULToFragment(`<box> </box>`).querySelector("box"); + is(boxWithWhitespaceText.textContent, "", "Whitespace removed"); + let boxWithNonWhitespaceText = MozXULElement.parseXULToFragment(`<box>foo</box>`).querySelector("box"); + is(boxWithNonWhitespaceText.textContent, "foo", "Non-whitespace not removed"); + + try { + // we didn't encode the & as & + MozXULElement.parseXULToFragment(`<box id="foo=1&bar=2"/>`); + ok(false, "parseXULToFragment should've thrown an exception for not-well-formed XML"); + } + catch (ex) { + is(ex.message, "not well-formed XML", "parseXULToFragment threw the wrong message"); + } + } + + function testInheritAttributes() { + class InheritsElementDeclarative extends MozXULElement { + static get inheritedAttributes() { + return { + "label": "text=label,foo,empty-string,bardo=bar", + "unmatched": "foo", // Make sure we don't throw on unmatched selectors + }; + } + + connectedCallback() { + this.textContent = ""; + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + this.initializeAttributeInheritance(); + } + } + customElements.define("inherited-element-declarative", InheritsElementDeclarative); + let declarativeEl = document.querySelector("inherited-element-declarative"); + ok(declarativeEl, "declarative inheritance element exists"); + + class InheritsElementDerived extends InheritsElementDeclarative { + static get inheritedAttributes() { + return { label: "renamedfoo=foo" }; + } + } + customElements.define("inherited-element-derived", InheritsElementDerived); + + class InheritsElementShadowDOMDeclarative extends MozXULElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + static get inheritedAttributes() { + return { + "label": "text=label,foo,empty-string,bardo=bar", + "unmatched": "foo", // Make sure we don't throw on unmatched selectors + }; + } + + connectedCallback() { + this.shadowRoot.textContent = ""; + this.shadowRoot.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.shadowRoot.querySelector("label"); + this.initializeAttributeInheritance(); + } + } + customElements.define("inherited-element-shadowdom-declarative", InheritsElementShadowDOMDeclarative); + let shadowDOMDeclarativeEl = document.querySelector("inherited-element-shadowdom-declarative"); + ok(shadowDOMDeclarativeEl, "declarative inheritance element with shadow DOM exists"); + + class InheritsElementImperative extends MozXULElement { + static get observedAttributes() { + return [ "label", "foo", "empty-string", "bar" ]; + } + + attributeChangedCallback(name, oldValue, newValue) { + if (this.label && oldValue != newValue) { + this.inherit(); + } + } + + inherit() { + let map = { + "label": [[ "label", "text" ]], + "foo": [[ "label", "foo" ]], + "empty-string": [[ "label", "empty-string" ]], + "bar": [[ "label", "bardo" ]], + }; + for (let attr of InheritsElementImperative.observedAttributes) { + this.inheritAttribute(map[attr], attr); + } + } + + connectedCallback() { + // Typically `initializeAttributeInheritance` handles this for us: + this._inheritedElements = null; + + this.textContent = ""; + this.append(MozXULElement.parseXULToFragment(`<label />`)); + this.label = this.querySelector("label"); + this.inherit(); + } + } + + customElements.define("inherited-element-imperative", InheritsElementImperative); + let imperativeEl = document.querySelector("inherited-element-imperative"); + ok(imperativeEl, "imperative inheritance element exists"); + + function checkElement(el) { + is(el.label.getAttribute("foo"), "fuagra", "predefined attribute @foo"); + ok(el.label.hasAttribute("empty-string"), "predefined attribute @empty-string"); + ok(!el.label.hasAttribute("bardo"), "predefined attribute @bardo"); + ok(!el.label.textContent, "predefined attribute @label"); + + el.setAttribute("empty-string", "not-empty-anymore"); + is(el.label.getAttribute("empty-string"), "not-empty-anymore", + "attribute inheritance: empty-string"); + + el.setAttribute("label", "label-test"); + is(el.label.textContent, "label-test", + "attribute inheritance: text=label attribute change"); + + el.setAttribute("bar", "bar-test"); + is(el.label.getAttribute("bardo"), "bar-test", + "attribute inheritance: `=` mapping"); + + el.label.setAttribute("bardo", "changed-from-child"); + is(el.label.getAttribute("bardo"), "changed-from-child", + "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was changed"); + + el.label.removeAttribute("bardo"); + ok(!el.label.hasAttribute("bardo"), + "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was removed"); + + el.setAttribute("bar", "changed-from-host"); + is(el.label.getAttribute("bardo"), "changed-from-host", + "attribute inheritance: does apply when host attr has changed and child attr was changed"); + + el.removeAttribute("bar"); + ok(!el.label.hasAttribute("bardo"), + "attribute inheritance: does apply when host attr has been removed"); + + el.setAttribute("bar", "changed-from-host-2"); + is(el.label.getAttribute("bardo"), "changed-from-host-2", + "attribute inheritance: does apply when host attr has changed after being removed"); + + // Restore to the original state so this can be ran again with the same element: + el.removeAttribute("label"); + el.removeAttribute("bar"); + } + + for (let el of [declarativeEl, shadowDOMDeclarativeEl, imperativeEl]) { + info(`Running checks for ${el.tagName}`); + checkElement(el); + info(`Remove and re-add ${el.tagName} to make sure attribute inheritance still works`); + el.replaceWith(el); + checkElement(el); + } + + let derivedEl = document.querySelector("inherited-element-derived"); + ok(derivedEl, "derived inheritance element exists"); + ok(!derivedEl.label.hasAttribute("foo"), + "attribute inheritance: base class attribute is not applied in derived class that overrides it"); + ok(derivedEl.label.hasAttribute("renamedfoo"), + "attribute inheritance: attribute defined in derived class is present"); + } + + async function testCustomInterface() { + class SimpleElement extends MozXULElement { + get disabled() { + return this.getAttribute("disabled") == "true"; + } + + set disabled(val) { + if (val) this.setAttribute("disabled", "true"); + else this.removeAttribute("disabled"); + } + + get tabIndex() { + return parseInt(this.getAttribute("tabIndex")) || 0; + } + + set tabIndex(val) { + if (val) this.setAttribute("tabIndex", val); + else this.removeAttribute("tabIndex"); + } + } + + MozXULElement.implementCustomInterface(SimpleElement, [Ci.nsIDOMXULControlElement]); + customElements.define("simpleelement", SimpleElement); + + let twoElement = document.getElementById("two"); + + is(document.documentElement.getCustomInterfaceCallback, undefined, + "No getCustomInterfaceCallback on non-custom element"); + is(typeof twoElement.getCustomInterfaceCallback, "function", + "getCustomInterfaceCallback available on custom element when set"); + is(document.documentElement.QueryInterface, undefined, + "Non-custom element should not have a QueryInterface implementation"); + + // Try various ways to get the custom interface. + + let asControl = twoElement.getCustomInterfaceCallback(Ci.nsIDOMXULControlElement); + + // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500967 + // Not sure if this was suppose to simply check for existence or equality? + todo_is(asControl, twoElement, "getCustomInterface returns interface implementation "); + + asControl = twoElement.QueryInterface(Ci.nsIDOMXULControlElement); + ok(asControl, "QueryInterface to nsIDOMXULControlElement"); + ok(Node.isInstance(asControl), "Control is a Node"); + + // Now make sure that the custom element handles focus/tabIndex as needed by shitfing + // focus around and enabling/disabling the simple elements. + + // Enable Full Keyboard Access emulation on Mac + await SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}); + + ok(!twoElement.disabled, "two is enabled"); + ok(document.getElementById("three").disabled, "three is disabled"); + + await SimpleTest.promiseFocus(); + ok(document.hasFocus(), "has focus"); + + // This should skip the disabled simpleelement. + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "one", "Tab 1"); + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "two", "Tab 2"); + synthesizeKey("VK_TAB"); + is(document.activeElement.id, "four", "Tab 3"); + + twoElement.disabled = true; + is(twoElement.getAttribute("disabled"), "true", "two disabled after change"); + + synthesizeKey("VK_TAB", { shiftKey: true }); + is(document.activeElement.id, "one", "Tab 1"); + + info("Checking that interfaces get inherited automatically with implementCustomInterface"); + class ExtendedElement extends SimpleElement { } + MozXULElement.implementCustomInterface(ExtendedElement, [Ci.nsIDOMXULSelectControlElement]); + customElements.define("extendedelement", ExtendedElement); + const extendedInstance = document.createXULElement("extendedelement"); + ok(extendedInstance.QueryInterface(Ci.nsIDOMXULSelectControlElement), "interface applied"); + ok(extendedInstance.QueryInterface(Ci.nsIDOMXULControlElement), "inherited interface applied"); + } + ]]> + </script> +</window> diff --git a/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml b/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml new file mode 100644 index 0000000000..f40277d198 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_delay_connection.xhtml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Custom Element Base Delayed Connected" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <script type="application/javascript"><![CDATA[ + let nativeDOMContentLoadedFired = false; + document.addEventListener("DOMContentLoaded", () => { + nativeDOMContentLoadedFired = true; + }, { once: true }); + + // To test `delayConnectedCallback` and `isConnectedAndReady` we have to run this before + // DOMContentLoaded, which is why this is done in a separate script that runs + // immediately and not in `runTests`. + let delayedConnectionPromise = new Promise(resolve => { + + let numSkippedAttributeChanges = 0; + let numDelayedConnections = 0; + let numDelayedDisconnections = 0; + let finishedWaitingForDOMReady = false; + + // Register this custom element before DOMContentLoaded has fired and before it's parsed in + // the markup: + customElements.define("delayed-connection", class DelayedConnection extends MozXULElement { + static get observedAttributes() { return ["foo"]; } + attributeChangedCallback() { + ok(!this.isConnectedAndReady, + "attributeChangedCallback fires before isConnectedAndReady"); + ok(!nativeDOMContentLoadedFired, + "attributeChangedCallback fires before nativeDOMContentLoadedFired"); + numSkippedAttributeChanges++; + } + connectedCallback() { + if (this.delayConnectedCallback()) { + ok(!finishedWaitingForDOMReady, + "connectedCallback with delayConnectedCallback fires before finishedWaitingForDOMReady"); + ok(!this.isConnectedAndReady, + "connectedCallback with delayConnectedCallback fires before isConnectedAndReady"); + ok(!nativeDOMContentLoadedFired, + "connectedCallback with delayConnectedCallback fires before nativeDOMContentLoadedFired"); + numDelayedConnections++; + return; + } + + ok(!finishedWaitingForDOMReady, + "connectedCallback only fires once when DOM is ready"); + ok(this.isConnectedAndReady, + "isConnectedAndReady during connectedCallback"); + ok(!nativeDOMContentLoadedFired, + "delayed connectedCallback fires before nativeDOMContentLoadedFired"); + + is(numSkippedAttributeChanges, 2, + "Correct number of skipped attribute changes"); + is(numDelayedConnections, 2, + "Correct number of delayed connections"); + is(numDelayedDisconnections, 1, + "Correct number of delated disconnections"); + + finishedWaitingForDOMReady = true; + resolve(); + } + disconnectedCallback() { + ok(this.delayConnectedCallback(), + "disconnectedCallback while DOM not ready"); + is(numDelayedDisconnections, 0, + "disconnectedCallback fired only once"); + numDelayedDisconnections++; + } + }); + }); + + // This should be called after the element is parsed below this. + function mutateDelayedConnection() { + // Fire connectedCallback and attributeChangedCallback twice before DOMContentLoaded + // fires. The first connectedCallback is due to the parse and the second due to re-appending. + let delayedConnection = document.querySelector("delayed-connection"); + delayedConnection.setAttribute("foo", "bar"); + delayedConnection.remove(); + delayedConnection.setAttribute("foo", "bat"); + document.documentElement.append(delayedConnection); + } + ]]> + </script> + + <delayed-connection></delayed-connection> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + mutateDelayedConnection(); + + async function runTests() { + info("Waiting for delayed connection to fire"); + ok(nativeDOMContentLoadedFired, + "nativeDOMContentLoadedFired is true in runTests"); + await delayedConnectionPromise; + SimpleTest.finish(); + } + ]]> + </script> +</window>
\ No newline at end of file diff --git a/toolkit/content/tests/chrome/test_custom_element_parts.html b/toolkit/content/tests/chrome/test_custom_element_parts.html new file mode 100644 index 0000000000..e46e93e206 --- /dev/null +++ b/toolkit/content/tests/chrome/test_custom_element_parts.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title><!-- Shadow Parts issue with xul/xbl domparser --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <style>custom-button::part(foo) { background: red; }</style> + <script> + add_task(async function test() { + // A simplified version of what MozXULElement.parseXULToFragment does + let parser = new DOMParser(); + parser.forceEnableXULXBL(); + let doc = parser.parseFromString(`<box xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><box part="foo">there</box></box>`, `application/xml`); + let range = doc.createRange(); + range.selectNodeContents(doc.querySelector("box")); + let frag = range.extractContents(); + + customElements.define("custom-button", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: "open"}); + this.shadowRoot.appendChild(document.importNode(frag, true)); + } + }); + let button = document.createElement("custom-button"); + document.body.appendChild(button); + + let box = button.shadowRoot.querySelector("box"); + + // XXX: this fixes it + // box.removeAttribute("part"); + // box.setAttribute("part", "foo"); + + is(window.getComputedStyle(box).getPropertyValue("background-color"), "rgb(255, 0, 0)", "part applied"); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_deck.xhtml b/toolkit/content/tests/chrome/test_deck.xhtml new file mode 100644 index 0000000000..d6f824b357 --- /dev/null +++ b/toolkit/content/tests/chrome/test_deck.xhtml @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for deck + --> +<window title="Deck Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<deck id="deck1" style="padding-top: 5px; padding-bottom: 12px;"> + <button id="d1b1" label="Button One"/> + <button id="d1b2" label="Button Two is larger" style="height: 80px; margin: 1px;"/> +</deck> +<deck id="deck2" selectedIndex="1"> + <button id="d2b1" label="Button One"/> + <button id="d2b2" label="Button Two"/> +</deck> +<deck id="deck3" selectedIndex="1"> + <button id="d3b1" label="Remove me"/> + <button id="d3b2" label="Keep me selected"/> +</deck> +<deck id="deck4" selectedIndex="5"> + <button id="d4b1" label="Remove me"/> + <button id="d4b2" label="Remove me"/> + <button id="d4b3" label="Remove me"/> + <button id="d4b4" label="Button 4"/> + <button id="d4b5" label="Button 5"/> + <button id="d4b6" label="Keep me selected"/> + <button id="d4b7" label="Button 7"/> +</deck> +<deck id="deck5" selectedIndex="2"> + <button id="d5b1" label="Button 1"/> + <button id="d5b2" label="Button 2"/> + <button id="d5b3" label="Keep me selected"/> + <button id="d5b4" label="Remove me"/> + <button id="d5b5" label="Remove me"/> + <button id="d5b6" label="Remove me"/> +</deck> + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +add_task(async function run_tests() { + test_deck(); + await test_deck_child_removal(); +}); + +function test_deck() +{ + var deck = $("deck1"); + is(deck.selectedIndex, 0, "deck one selectedIndex"); + // this size is the button height, 80, plus the button padding of 1px on each side, + // plus the deck's 5px top padding and the 12px bottom padding. + var rect = deck.getBoundingClientRect(); + is(Math.round(rect.bottom) - Math.round(rect.top), 99, "deck size of largest child"); + synthesizeMouseExpectEvent(deck, 12, 12, { }, $("d1b1"), "click", "mouse on deck one"); + + // change the selected page of the deck and ensure that the mouse click goes + // to the button on that page + deck.selectedIndex = 1; + is(deck.selectedIndex, 1, "deck one selectedIndex after change"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d1b2"), "click", "mouse on deck one after change"); + + deck = $("deck2"); + is(deck.selectedIndex, 1, "deck two selectedIndex"); + synthesizeMouseExpectEvent(deck, 9, 9, { }, $("d2b2"), "click", "mouse on deck two"); +} + +async function test_deck_child_removal() +{ + // Start with a simple case where we have two child nodes in a deck, with + // the second child (index 1) selected. Removing the first node should + // automatically set the selectedIndex at 0. + let deck = $("deck3"); + let child = $("d3b1"); + is(deck.selectedIndex, 1, "Should have the deck element at index 1 selected"); + + // Remove the child at the 0th index. The deck should automatically + // set the selectedIndex to "0". + child.remove(); + + await Promise.resolve(); + + is(deck.selectedIndex, 0, "Should have the deck element at index 0 selected"); + + // Now scale it up by using a deck with 7 child nodes, and remove the + // first three, making sure that the selectedIndex is decremented + // each time. + deck = $("deck4"); + let expectedIndex = 5; + is(deck.selectedIndex, expectedIndex, + "Should have the deck element at index " + expectedIndex + " selected"); + + for (let i = 0; i < 3; ++i) { + deck.firstChild.remove(); + expectedIndex--; + await Promise.resolve(); + is(deck.selectedIndex, expectedIndex, + "Should have the deck element at index " + expectedIndex + " selected"); + } + + // Check that removing the currently selected node doesn't change + // behaviour. + deck.childNodes[expectedIndex].remove(); + await Promise.resolve(); + is(deck.selectedIndex, expectedIndex, + "The selectedIndex should not change when removing the node " + + "at the selected index."); + + // Finally, make sure we haven't changed the behaviour when removing + // nodes at indexes greater than the selected node. + deck = $("deck5"); + expectedIndex = 2; + await Promise.resolve(); + is(deck.selectedIndex, expectedIndex, + "Should have the deck element at index " + expectedIndex + " selected"); + + // And then remove all of the nodes, starting from last to first, making + // sure that the selectedIndex does not change. + while (deck.lastChild) { + deck.lastChild.remove(); + await Promise.resolve(); + is(deck.selectedIndex, expectedIndex, + "Should have the deck element at index " + expectedIndex + " selected"); + } +} +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_dialog_button.xhtml b/toolkit/content/tests/chrome/test_dialog_button.xhtml new file mode 100644 index 0000000000..8c973839c1 --- /dev/null +++ b/toolkit/content/tests/chrome/test_dialog_button.xhtml @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title><!-- Test with dialog & buttons --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script><![CDATA[ + add_task(async function test_dialog_button_accesskey() { + var win = window.browsingContext.topChromeWindow.openDialog( + "dialog_button.xhtml", + "_new", + "chrome,dialog" + ); + await new Promise((r) => win.addEventListener("load", r, { once: true })); + + let dialogClosed = new Promise((r) => { + win.document.addEventListener("dialogclosing", r, { once: true }); + }); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1625632 + // When pressing an accesskey for a built in dialog button while a regular button is focused, + // it should forward to the dialog. + win.document.querySelector("#button").focus(); + synthesizeKey("a", {}, win); + await dialogClosed; + ok(true, "Accesskey on focused button"); + }); + ]]></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_dialogfocus.xhtml b/toolkit/content/tests/chrome/test_dialogfocus.xhtml new file mode 100644 index 0000000000..10e60ac776 --- /dev/null +++ b/toolkit/content/tests/chrome/test_dialogfocus.xhtml @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<button id="test" label="Test"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +var expected = [ "one", "_extra2", "tab", "one", "tabbutton2", "tabbutton", "two", "textbox-yes", "one", "root" ]; +// non-Mac will always focus the default button if any of the dialog buttons +// would be focused +if (!navigator.platform.includes("Mac")) + expected[1] = "_accept"; + +let extraDialog = "data:application/xhtml+xml,<window id='root'><dialog " + + "buttons='none' " + + "xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'>" + + "<button id='nonbutton' noinitialfocus='true'/></dialog></window>"; + +var step = 0; +var fullKeyboardAccess = false; + +function startTest() +{ + var testButton = document.getElementById("test"); + synthesizeKey("KEY_Tab"); + fullKeyboardAccess = (document.activeElement == testButton); + info("We " + (fullKeyboardAccess ? "have" : "don't have") + " full keyboard access"); + runTest(); +} + +function runTest() +{ + step++; + info("runTest(), step = " + step + ", expected = " + expected[step - 1]); + if (step > expected.length || (!fullKeyboardAccess && step == 2)) { + info("finishing"); + SimpleTest.finish(); + return; + } + + var expectedFocus = expected[step - 1]; + let filename = expectedFocus == "root" ? "dialog_dialogfocus2.xhtml" : "dialog_dialogfocus.xhtml"; + var win = window.browsingContext.topChromeWindow.openDialog(filename, "_new", "chrome,dialog", step); + + function checkDialogFocus(event) + { + info(`checkDialogFocus()`); + let match = false; + let activeElement = win.document.activeElement; + let dialog = win.document.getElementById("dialog-focus"); + + if (activeElement == dialog) { + let shadowActiveElement = + dialog.shadowRoot.activeElement; + if (shadowActiveElement) { + activeElement = shadowActiveElement; + } + } + // if full keyboard access is not on, just skip the tests + if (fullKeyboardAccess) { + if (!(Element.isInstance(event.target))) { + info("target not an Element"); + return; + } + + if (expectedFocus[0] == "_") + match = (activeElement.getAttribute("dlgtype") == expectedFocus.substring(1)); + else + match = (activeElement.id == expectedFocus); + info("match = " + match); + if (!match) + return; + } + else { + match = (activeElement == win.document.documentElement); + info("match = " + match); + } + + win.removeEventListener("focus", checkDialogFocusEvent, true); + dialog.shadowRoot.removeEventListener( + "focus", checkDialogFocusEvent, true); + ok(match, "focus step " + step); + + win.close(); + SimpleTest.waitForFocus(runTest, window); + } + + let finalCheckInitiated = false; + function checkDialogFocusEvent(event) { + // Delay to have time for focus/blur to occur. + if (expectedFocus == "root") { + if (!finalCheckInitiated) { + setTimeout(() => { + is(win.document.activeElement, win.document.documentElement, + "No other focus but root"); + win.close(); + SimpleTest.waitForFocus(runTest, window); + }, 0); + finalCheckInitiated = true; + } + } else { + checkDialogFocus(event); + } + } + win.addEventListener("focus", checkDialogFocusEvent, true); + win.addEventListener("load", () => { + win.document.getElementById("dialog-focus").shadowRoot.addEventListener( + "focus", checkDialogFocusEvent, true); + }); +} + +SimpleTest.waitForFocus(startTest, window); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_edit_contextmenu.html b/toolkit/content/tests/chrome/test_edit_contextmenu.html new file mode 100644 index 0000000000..88140efa9c --- /dev/null +++ b/toolkit/content/tests/chrome/test_edit_contextmenu.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1513343 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1513343</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTest() { + let win = window.browsingContext.topChromeWindow.open("file_edit_contextmenu.xhtml", "_blank", "chrome,width=600,height=600"); + await new Promise(r => win.addEventListener("load", r, { once: true })); + await SimpleTest.promiseFocus(win); + + const elements = [ + win.document.querySelector("textarea"), + win.document.querySelector("input"), + win.document.querySelector("search-textbox"), + win.document.querySelector("shadow-input").shadowRoot.querySelector("input"), + ]; + for (const element of elements) { + await testElement(element, win); + } + win.close(); + SimpleTest.finish(); + } + + async function testElement(element, win) { + ok(element, "element exists"); + + info("Synthesizing a key so 'Undo' will be enabled"); + element.focus(); + synthesizeKey("x", {}, win); + is(element.value, "x", "initial value"); + + element.select(); + synthesizeKey("c", { accelKey: true }, win); // copy to clipboard + synthesizeKey("KEY_ArrowRight", {}, win); // drop selection to disable cut and copy context menu items + + win.document.addEventListener("contextmenu", (e) => { + info("Calling prevent default on the first contextmenu event"); + e.preventDefault(); + }, { once: true }); + synthesizeMouseAtCenter(element, {type: "contextmenu"}, win); + ok(!win.document.getElementById("textbox-contextmenu"), "contextmenu with preventDefault() doesn't run"); + + let popupshown = new Promise(r => win.addEventListener("popupshown", r, { once: true })); + synthesizeMouseAtCenter(element, {type: "contextmenu"}, win); + let contextmenu = win.document.getElementById("textbox-contextmenu"); + ok(contextmenu, "context menu exists after right click"); + await popupshown; + + // Check that we only got the one context menu, and not two. + let outerContextmenu = win.document.getElementById("outer-context-menu"); + ok(outerContextmenu.state == "closed", "the outer context menu state is is not closed, it's: " + outerContextmenu.state); + + ok(!contextmenu.querySelector("[command=cmd_undo]").hasAttribute("disabled"), "undo enabled"); + ok(contextmenu.querySelector("[command=cmd_cut]").hasAttribute("disabled"), "cut disabled"); + ok(contextmenu.querySelector("[command=cmd_copy]").hasAttribute("disabled"), "copy disabled"); + ok(!contextmenu.querySelector("[command=cmd_paste]").hasAttribute("disabled"), "paste enabled"); + ok(contextmenu.querySelector("[command=cmd_delete]").hasAttribute("disabled"), "delete disabled"); + ok(!contextmenu.querySelector("[command=cmd_selectAll]").hasAttribute("disabled"), "select all enabled"); + + let popuphidden = new Promise(r => win.addEventListener("popuphidden", r, { once: true })); + + contextmenu.activateItem(contextmenu.querySelector("[command=cmd_undo]")); + + await popuphidden; + + is(element.value, "", "undo worked"); + contextmenu.remove(); + } + </script> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1513343">Mozilla Bug 1513343</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html new file mode 100644 index 0000000000..91f0f159b4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_editor_for_input_with_autocomplete.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Basic editor behavior for HTML input element with autocomplete</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="file_editor_with_autocomplete.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="data:text/html," target="formTarget"> + <input name="test" id="input"><input type="submit"> + </form> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function registerWord(aTarget, aAutoCompleteController) { + // Register a word to the form history. + let chromeScript = SpecialPowers.loadChromeScript(function addEntry() { +/* eslint-env mozilla/chrome-script */ + let {FormHistory} = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" + ); + FormHistory.update({ op: "add", fieldname: "test", value: "Mozilla" }); + }); + aTarget.focus(); + aTarget.value = "Mozilla"; + + await waitForCondition(() => { + if (aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_NONE || + aAutoCompleteController.searchStatus == aAutoCompleteController.STATUS_COMPLETE_NO_MATCH) { + aAutoCompleteController.startSearch("Mozilla"); + } + return aAutoCompleteController.matchCount > 0; + }); + chromeScript.destroy(); + aTarget.value = ""; + synthesizeKey("KEY_Escape"); +} + +async function runTests() { + var formFillController = + SpecialPowers.getFormFillController() + .QueryInterface(Ci.nsIAutoCompleteInput); + var originalFormFillTimeout = formFillController.timeout; + + SpecialPowers.attachFormFillControllerTo(window); + var target = document.getElementById("input"); + + // Register a word to the form history. + await registerWord(target, formFillController.controller); + + let tests1 = new nsDoTestsForEditorWithAutoComplete( + "Testing on HTML input (asynchronously search)", + window, target, formFillController.controller, is, todo_is, + function() { return target.value; }); + await tests1.run(); + + target.setAttribute("timeout", 0); + let tests2 = new nsDoTestsForEditorWithAutoComplete( + "Testing on HTML input (synchronously search)", + window, target, formFillController.controller, is, todo_is, + function() { return target.value; }); + await tests2.run(); + + formFillController.timeout = originalFormFillTimeout; + SpecialPowers.detachFormFillControllerFrom(window); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_findbar.xhtml b/toolkit/content/tests/chrome/test_findbar.xhtml new file mode 100644 index 0000000000..a0329adfbd --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar.xhtml @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=257061 +https://bugzilla.mozilla.org/show_bug.cgi?id=288254 +--> +<window title="Mozilla Bug 257061 and Bug 288254" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=257061">Mozilla Bug 257061</a> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=288254">Mozilla Bug 288254</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +/** Test for Bug 257061 and Bug 288254 **/ +SimpleTest.waitForExplicitFinish(); + +// Since bug 978861, this pref is set to `false` on OSX. For this test, we'll +// set it `true` to disable the find clipboard on OSX, which interferes with +// our tests. +SpecialPowers.pushPrefEnv({ + set: [["accessibility.typeaheadfind.prefillwithselection", true]] +}, () => { + window.openDialog("findbar_window.xhtml", "findbartest", "chrome,width=600,height=600,noopener", window); +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_entireword.xhtml b/toolkit/content/tests/chrome/test_findbar_entireword.xhtml new file mode 100644 index 0000000000..641e407aa0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_entireword.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=269442 +--> +<window title="Mozilla Bug 269442" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=269442"> + Mozilla Bug 269442 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 269442 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("findbar_entireword_window.xhtml", "269442test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_findbar_events.xhtml b/toolkit/content/tests/chrome/test_findbar_events.xhtml new file mode 100644 index 0000000000..f8e96d8ba8 --- /dev/null +++ b/toolkit/content/tests/chrome/test_findbar_events.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet + href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=793275 +--> +<window title="Mozilla Bug 793275" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=793275"> + Mozilla Bug 793275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + /** Test for Bug 793275 **/ + SimpleTest.waitForExplicitFinish(); + window.openDialog("findbar_events_window.xhtml", "793275test", + "chrome,width=600,height=600,noopener", window); + + ]]> + </script> + +</window> diff --git a/toolkit/content/tests/chrome/test_frames.xhtml b/toolkit/content/tests/chrome/test_frames.xhtml new file mode 100644 index 0000000000..a849b1d6b0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_frames.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script><![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function runTest() { + for (let i = 1; i <= 3; i++) { + let frame = document.getElementById("frame" + i); + ok(XULFrameElement.isInstance(frame), "XULFrameElement " + i); + + // Check the various fields to ensure that they have the correct type. + ok(frame.docShell instanceof Ci.nsIDocShell, "docShell " + i); + ok(frame.webNavigation instanceof Ci.nsIWebNavigation, "webNavigation " + i); + + let contentWindow = frame.contentWindow; + let contentDocument = frame.contentDocument; + ok(Window.isInstance(contentWindow), "contentWindow " + i); + ok(Document.isInstance(contentDocument), "contentDocument " + i); + is(contentDocument.body.id, "thechildbody" + i, "right document body " + i); + + // These fields should all be read-only. + frame.docShell = null; + ok(frame.docShell instanceof Ci.nsIDocShell, "docShell after set " + i); + frame.webNavigation = null; + ok(frame.webNavigation instanceof Ci.nsIWebNavigation, "webNavigation after set " + i); + frame.contentWindow = window; + is(frame.contentWindow, contentWindow, "contentWindow after set " + i); + frame.contentDocument = document; + is(frame.contentDocument, contentDocument, "contentDocument after set " + i); + } + + // A non-frame element should not have these fields. + let button = document.getElementById("nonframe"); + ok(!(XULFrameElement.isInstance(button)), "XULFrameElement non frame"); + is(button.docShell, undefined, "docShell non frame"); + is(button.webNavigation, undefined, "webNavigation non frame"); + is(button.contentWindow, undefined, "contentWindow non frame"); + is(button.contentDocument, undefined, "contentDocument non frame"); + + SimpleTest.finish(); +} +]]> +</script> + +<iframe id="frame1" src="data:text/html,<body id='thechildbody1'>"/> +<browser id="frame2" src="data:text/html,<body id='thechildbody2'>"/> +<editor id="frame3" src="data:text/html,<body id='thechildbody3'>"/> +<button id="nonframe"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<div id="content" style="display: none"></div> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenitems.xhtml b/toolkit/content/tests/chrome/test_hiddenitems.xhtml new file mode 100644 index 0000000000..79f06c8890 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenitems.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7" hidden="true"><label value="Item 7"/></richlistitem> + </richlistbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testListbox(id) +{ + var listbox = document.getElementById(id); + listbox.focus(); + is(listbox.getRowCount(), 7, id + ": Returned the wrong number of rows"); + is(listbox.getItemAtIndex(2).id, id + "_item3", id + ": Should still return hidden items"); + listbox.selectedIndex = 0; + is(listbox.selectedItem.id, id + "_item1", id + ": First item was not selected"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item2", id + ": Down didn't move to second item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item4", id + ": Down didn't skip hidden item"); + sendKey("DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Down didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item4", id + ": Up didn't skip collapsed item"); + sendKey("UP"); + is(listbox.selectedItem.id, id + "_item2", id + ": Up didn't skip hidden item"); + listbox.selectAll(); + is(listbox.selectedItems.length, 7, id + ": Should have still selected all items"); + listbox.selectedIndex = 2; + ok(listbox.selectedItem == listbox.getItemAtIndex(2), id + ": Should have selected the hidden item"); + listbox.selectedIndex = 0; + sendKey("END"); + is(listbox.selectedItem.id, id + "_item6", id + ": Should have moved to the last unhidden item"); + sendMouseEvent({type: 'click'}, id + "_item1"); + ok(listbox.selectedItem == listbox.getItemAtIndex(0), id + ": Should have selected the first item"); + is(listbox.selectedItems.length, 1, id + ": Should only be one selected item"); + sendMouseEvent({type: 'click', shiftKey: true}, id + "_item6"); + is(listbox.selectedItems.length, 4, id + ": Should have selected all visible items"); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item6", id + ": Page down should go to the last visible item"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Page up should go to the first visible item"); +} + +window.onload = function runTests() { + testListbox("richlistbox"); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_hiddenpaging.xhtml b/toolkit/content/tests/chrome/test_hiddenpaging.xhtml new file mode 100644 index 0000000000..59bcd0e432 --- /dev/null +++ b/toolkit/content/tests/chrome/test_hiddenpaging.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=317422 +--> +<window title="Mozilla Bug 317422" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <style xmlns="http://www.w3.org/1999/xhtml"> + /* This makes the richlistbox about 4.5 rows high */ + richlistitem:not([collapsed]) { + min-height: 30px; + } + richlistbox { + height: 135px; + } + </style> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=317422" + target="_blank">Mozilla Bug 317422</a> + </body> + + <richlistbox id="richlistbox" seltype="multiple"> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3" hidden="true"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5" collapsed="true"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + <richlistitem id="richlistbox_item9"><label value="Item 9"/></richlistitem> + <richlistitem id="richlistbox_item10"><label value="Item 10"/></richlistitem> + <richlistitem id="richlistbox_item11"><label value="Item 11"/></richlistitem> + <richlistitem id="richlistbox_item12"><label value="Item 12"/></richlistitem> + <richlistitem id="richlistbox_item13"><label value="Item 13"/></richlistitem> + <richlistitem id="richlistbox_item14"><label value="Item 14"/></richlistitem> + <richlistitem id="richlistbox_item15" hidden="true"><label value="Item 15"/></richlistitem> + </richlistbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 317422 **/ +SimpleTest.waitForExplicitFinish(); + +function testRichlistbox() +{ + var id = "richlistbox"; + var listbox = document.getElementById(id); + listbox.focus(); + listbox.selectedIndex = 0; + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item7", id + ": Page down should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 6, id + ": Page down should have scrolled down a visible page"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item11", id + ": Second page down should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Second page down should not scroll beyond the end"); + sendKey("PAGE_DOWN"); + is(listbox.selectedItem.id, id + "_item14", id + ": Third page down should go to the last visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 9, id + ": Third page down should not have scrolled at all"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item10", id + ": Page up should go to the item one visible page away"); + is(listbox.getIndexOfFirstVisibleRow(), 5, id + ": Page up should scroll up a visible page"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item6", id + ": Second page up should go to the item two visible pages away"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Second page up should not scroll beyond the start"); + sendKey("PAGE_UP"); + is(listbox.selectedItem.id, id + "_item1", id + ": Third page up should return to the first visible item"); + is(listbox.getIndexOfFirstVisibleRow(), 0, id + ": Third page up should not have scrolled at all"); +} + +window.onload = function runTests() { + testRichlistbox(); + SimpleTest.finish(); +}; + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_keys.xhtml b/toolkit/content/tests/chrome/test_keys.xhtml new file mode 100644 index 0000000000..4d4c1ae8f0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_keys.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Keys Test" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_keys.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_labelcontrol.xhtml b/toolkit/content/tests/chrome/test_labelcontrol.xhtml new file mode 100644 index 0000000000..06e7f96105 --- /dev/null +++ b/toolkit/content/tests/chrome/test_labelcontrol.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for label control="value" + --> +<window title="tabindex" width="500" height="600" + onload="runTests()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<label id="textbox-label" control="textbox" /> +<html:input id="textbox" value="Test"/> +<label id="checkbox-label" control="checkbox" /> +<checkbox id="checkbox" value="Checkbox"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + let textbox = $("textbox"); + let textboxLabel = $("textbox-label"); + is(textboxLabel.control, "textbox", "textbox control"); + is(textboxLabel.labeledControlElement, textbox, "textbox labeledControlElement"); + + let checkbox = $("checkbox"); + let checkboxLabel = $("checkbox-label"); + is(checkboxLabel.control, "checkbox", "checkbox control"); + is(checkboxLabel.labeledControlElement, checkbox, "checkbox labeledControlElement"); + is(checkbox.accessKey, "", "checkbox accessKey not set"); + checkboxLabel.accessKey = "C"; + is(checkbox.accessKey, "C", "checkbox accessKey set"); + + SimpleTest.finish(); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_largemenu.html b/toolkit/content/tests/chrome/test_largemenu.html new file mode 100644 index 0000000000..eb6b6eff8e --- /dev/null +++ b/toolkit/content/tests/chrome/test_largemenu.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Large Menu Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + async function runTest() { + // This test exercises non-native menu code. So disable native context menus for this test. + // If we ever get to a point where we don't use any non-native menus on macOS any more, we can + // disable this test on macOS. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + + window.openDialog("window_largemenu.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); + } + </script> +</head> +<body onload="setTimeout(runTest, 0);"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_maximized_persist.xhtml b/toolkit/content/tests/chrome/test_maximized_persist.xhtml new file mode 100644 index 0000000000..1e2e648e4d --- /dev/null +++ b/toolkit/content/tests/chrome/test_maximized_persist.xhtml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Window Open Test" + onload="runTest('window_maximized_persist.xhtml')" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<script src="file_maximized_persist.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</window> diff --git a/toolkit/content/tests/chrome/test_maximized_persist_with_no_titlebar.xhtml b/toolkit/content/tests/chrome/test_maximized_persist_with_no_titlebar.xhtml new file mode 100644 index 0000000000..3bc36c8e82 --- /dev/null +++ b/toolkit/content/tests/chrome/test_maximized_persist_with_no_titlebar.xhtml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Window Open Test" + onload="runTest('window_maximized_persist_with_no_titlebar.xhtml')" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<script src="file_maximized_persist.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</window> diff --git a/toolkit/content/tests/chrome/test_menu.xhtml b/toolkit/content/tests/chrome/test_menu.xhtml new file mode 100644 index 0000000000..8ef5c4ca15 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menubar> + <menu label="top" id="top"> + <menupopup> + <menuitem label="top item"/> + + <menu label="hello" id="nested"> + <menupopup> + <menuitem label="item1"/> + <menuitem label="item2" id="item2"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTests() + { + var menu = document.getElementById("nested"); + + // nsIDOMXULContainerElement::getIndexOfItem(); + var item = document.getElementById("item2"); + is(menu.getIndexOfItem(item), 1, + "nsIDOMXULContainerElement::getIndexOfItem() failed."); + + // nsIDOMXULContainerElement::getItemAtIndex(); + var itemAtIdx = menu.getItemAtIndex(1); + is(itemAtIdx, item, + "nsIDOMXULContainerElement::getItemAtIndex() failed."); + + // nsIDOMXULContainerElement::itemCount + is(menu.itemCount, 2, "nsIDOMXULContainerElement::itemCount failed."); + + // nsIDOMXULContainerElement::parentContainer + var topmenu = document.getElementById("top"); + is(menu.parentContainer, topmenu, + "nsIDOMXULContainerElement::parentContainer failed."); + + // nsIDOMXULContainerElement::appendItem(); + item = menu.appendItem("item3"); + is(menu.getIndexOfItem(item), 2, + "nsIDOMXULContainerElement::appendItem() failed."); + + SimpleTest.finish(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + +</window> + diff --git a/toolkit/content/tests/chrome/test_menu_activateitem.xhtml b/toolkit/content/tests/chrome/test_menu_activateitem.xhtml new file mode 100644 index 0000000000..8b4bff89d6 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_activateitem.xhtml @@ -0,0 +1,169 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Activation Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <label id="label" value="label" context="contextmenu"/> + <menupopup id="contextmenu"> + <menuitem id="item1" label="Item 1"/> + <menu label="Submenu"> + <menupopup id="submenu"> + <menuitem id="item2" label="Item 2"/> + <menuitem id="item4" label="Item 4" hidden="true"/> + <menu label="Inner Submenu"> + <menupopup id="innersubmenu"> + <menuitem id="item3" label="Item 3"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + + <script class="testbody" type="application/javascript"> + <![CDATA[ + +function waitForEvent(subject, eventName) { + return new Promise(resolve => { + subject.addEventListener(eventName, function listener(event) { + if (event.target == subject) { + subject.removeEventListener(eventName, listener); + resolve(); + } + }); + }); +} + +let doNothing = () => {}; + +function checkActivate(desc, menu, item, expectedResult) +{ + desc = desc + " for " + menu.id + ".activateItem(" + item.id + ")"; + try { + menu.activateItem(item); + ok(expectedResult, desc); + } catch (ex) { + ok(!expectedResult, `${desc}: ${ex}`); + } +} + +async function checkActivateItems(desc, openFn, expectedResults) +{ + // Iterate over each menu/submenu and try activating the item from that menu. + // This should only pass when the item is a descendant of that menu and the + // menu is open. + let menus = [ "contextmenu", "submenu", "innersubmenu" ]; + let items = [ "item1", "item2", "item3", "item4" ]; + + let needToOpen = true; + + let contextmenu = document.getElementById("contextmenu"); + + for (let m = 0; m < menus.length; m++) { + let menu = document.getElementById(menus[m]); + + for (let i = 0; i < items.length; i++) { + if (needToOpen) { + await openFn(); + } + + // If an activation is expected, the popup will hide. Wait for it to + // hide after calling checkActivate. If the popup is hidden, it will + // need to be reopened for the next step, so set needToOpen accordingly. + let popupHiddenPromise; + needToOpen = expectedResults[i]; + if (expectedResults[i]) { + popupHiddenPromise = waitForEvent(contextmenu, "popuphidden"); + } + + checkActivate(desc, menu, document.getElementById(items[i]), expectedResults[i]); + + await popupHiddenPromise; + } + + // For the next iteration, we will be using the next submenu. The item + // in the first menu will never be activatable since it is in a higher + // part of the menu hierarchy. + expectedResults[m] = false; + } + + if (!needToOpen && openFn != doNothing) { + // Hide the popup if it is still expected to be open. If doNothing is + // the opening function, then the popup would never have been opened. + let popupHiddenPromise = waitForEvent(contextmenu, "popuphidden"); + contextmenu.hidePopup(); + await popupHiddenPromise; + } +} + +add_task(async function () { + await checkActivateItems("expected failure when no menus open", + doNothing, [false, false, false, false]); + + let openContextMenu = async () => { + // Open the first level of the context menu + let contextmenu = document.getElementById("contextmenu"); + let popupShownPromise = waitForEvent(contextmenu, "popupshown"); + synthesizeMouseAtCenter(document.getElementById("label"), { + type: "contextmenu", + button: 2, + }); + await popupShownPromise; + } + + await checkActivateItems("one menu open", openContextMenu, [true, false, false, false]); + + // Open the second level of the context menu + let openSubmenu = async () => { + await openContextMenu(); + let submenu = document.getElementById("submenu"); + let popupShownPromise = waitForEvent(submenu, "popupshown"); + submenu.parentNode.openMenu(true); + await popupShownPromise; + } + + await checkActivateItems("submenu menu open", openSubmenu, [true, true, false, false]); + + // Open the last level of the context menu + let openInnerSubmenu = async () => { + await openSubmenu(); + + let innersubmenu = document.getElementById("innersubmenu"); + let popupShownPromise = waitForEvent(innersubmenu, "popupshown"); + innersubmenu.parentNode.openMenu(true); + await popupShownPromise; + }; + await checkActivateItems("inner submenu menu open", openInnerSubmenu, [true, true, true, false]); + + // Check that an item not in the menu is not valid. + await openInnerSubmenu(); + + let innersubmenu = document.getElementById("innersubmenu"); + await checkActivate("item not in menu", innersubmenu, document.getElementById("item1"), false); + + // Now check with all menus closed again + let contextmenu = document.getElementById("contextmenu"); + let popupHiddenPromise = waitForEvent(contextmenu, "popuphidden"); + contextmenu.hidePopup(); + await popupHiddenPromise; + + await checkActivateItems("all menus closed", doNothing, [false, false, false, false]); +}); + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"> + </p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menu_hide.xhtml b/toolkit/content/tests/chrome/test_menu_hide.xhtml new file mode 100644 index 0000000000..c8698bf572 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_hide.xhtml @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Destruction Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menu id="menu"> + <menupopup onpopuphidden="if (event.target == this) done()"> + <menu id="submenu" label="One"> + <menupopup onpopupshown="submenuOpened();"> + <menuitem label="Two"/> + </menupopup> + </menu> + </menupopup> +</menu> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + let menu = $("menu"); + let menuitemAddedWhileHidden = menu.appendItem("Added while hidden"); + ok(!menuitemAddedWhileHidden.querySelector(".menu-text"), "hidden menuitem hasn't rendered yet."); + + menu.menupopup.addEventListener("popupshown", () => { + is(menuitemAddedWhileHidden.querySelector(".menu-text").value, "Added while hidden", + "menuitemAddedWhileHidden item has rendered after shown."); + let menuitemAddedWhileShown = menu.appendItem("Added while shown"); + is(menuitemAddedWhileShown.querySelector(".menu-text").value, "Added while shown", + "menuitemAddedWhileShown item has eagerly rendered."); + + let submenu = $("submenu"); + is(submenu.querySelector(".menu-text").value, "One", "submenu has rendered."); + + let submenuDynamic = document.createXULElement("menu"); + submenuDynamic.setAttribute("label", "Dynamic"); + ok(!submenuDynamic.querySelector(".menu-text"), "dynamic submenu hasn't rendered yet."); + menu.menupopup.append(submenuDynamic); + is(submenuDynamic.querySelector(".menu-text").value, "Dynamic", "dynamic submenu has rendered."); + + menu.menupopup.firstElementChild.open = true; + }, { once: true }); + menu.open = true; +} + +function submenuOpened() +{ + let submenu = $("submenu"); + is(submenu.getAttribute('_moz-menuactive'), "true", "menu highlighted"); + submenu.hidden = true; + $("menu").open = false; +} + +function done() +{ + ok($("submenu").hasAttribute('_moz-menuactive'), "menu still highlighted"); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menu_mouse_menuactive.xhtml b/toolkit/content/tests/chrome/test_menu_mouse_menuactive.xhtml new file mode 100644 index 0000000000..edd7f582b5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_mouse_menuactive.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Activation Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <label id="label" value="label" context="contextmenu"/> + <menupopup id="contextmenu"> + <menuitem id="item1" label="Item 1"/> + <menuitem id="item2" label="Item 2"/> + </menupopup> + <script><![CDATA[ + +function waitForEvent(subject, eventName) { + return new Promise(resolve => { + subject.addEventListener(eventName, function listener(event) { + if (event.target == subject) { + subject.removeEventListener(eventName, listener); + resolve(); + } + }); + }); +} + +const menu = document.getElementById("contextmenu"); +const label = document.getElementById("label"); +const item1 = document.getElementById("item1"); +const item2 = document.getElementById("item2"); + +function openContextMenu() { + // Open the first level of the context menu + let promise = waitForEvent(menu, "popupshown"); + synthesizeMouseAtCenter(label, { + type: "contextmenu", + button: 2, + }); + return promise; +} + +function isActive(item) { + return item.hasAttribute("_moz-menuactive"); +} + +async function activateItem(item) { + info(`Activating ${item.id}`); + ok(!isActive(item), "Shouldn't be already active"); + let activated = waitForEvent(item, "DOMMenuItemActive"); + synthesizeMouse(item, 5, 5, { type: "mousemove" }); + await new Promise(r => setTimeout(r, 0)); + synthesizeMouse(item, 7, 7, { type: "mousemove" }); + await activated; +} + +add_task(async function() { + // Disable macOS native context menus, since we can't control activation of those here. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + info(`Opening context-menu`); + await openContextMenu(); + is(menu.state, "open", "Menu should be open"); + + await activateItem(item1); + ok(isActive(item1), "item1 should be active"); + ok(!isActive(item2), "item2 should be inactive"); + + await activateItem(item2); + ok(isActive(item2), "item2 should be active"); + ok(!isActive(item1), "item1 should be inactive"); + + await activateItem(item1); + ok(isActive(item1), "item1 should be active"); + ok(!isActive(item2), "item2 should be inactive"); + + info(`Leaving context-menu`); + + let deactivated = waitForEvent(item1, "DOMMenuItemInactive"); + synthesizeMouse(label, 0, 0, { type: "mousemove" }); + + await deactivated; + ok(!isActive(item1), "item1 should be inactive"); + ok(!isActive(item2), "item2 should be inactive"); + + is(menu.state, "open", "Menu should still be open"); + menu.hidePopup(); +}); + + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_menu_withcapture.xhtml b/toolkit/content/tests/chrome/test_menu_withcapture.xhtml new file mode 100644 index 0000000000..fe90e3201c --- /dev/null +++ b/toolkit/content/tests/chrome/test_menu_withcapture.xhtml @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu with Mouse Capture" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menulist id="menulist"> + <menupopup onpopupshown="shown(this)" onpopuphidden="done();"> + <menuitem id="menuitem" label="Menu Item" + onmousemove="moveHappened = true;" + onmouseup="upHappened = true;"/> + </menupopup> + </menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +let moveHappened = false, upHappened = false; + +function startTest() { + disableNonTestMouseEvents(true); + document.getElementById("menulist"). open = true; +} + +function shown(popup) +{ + popup.setCaptureAlways(); + setTimeout(function() { + synthesizeMouseAtCenter(document.getElementById("menuitem"), { type: "mousemove" }); + synthesizeMouseAtCenter(document.getElementById("menuitem"), { type: "mouseup" }); + }, 200); +} + +function done() +{ + ok(moveHappened, "mousemove happened"); + ok(upHappened, "mouseup happened"); + disableNonTestMouseEvents(false); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuchecks.xhtml b/toolkit/content/tests/chrome/test_menuchecks.xhtml new file mode 100644 index 0000000000..8e67c9bb10 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuchecks.xhtml @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Checkbox and Radio Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <button id="menu" type="menu" label="View"> + <menupopup id="popup" onpopupshown="popupShown()" onpopuphidden="popupHidden()"> + <menuitem id="toolbar" label="Show Toolbar" type="checkbox"/> + <menuitem id="statusbar" label="Show Status Bar" type="checkbox" checked="true"/> + <menuitem id="bookmarks" label="Show Bookmarks" type="checkbox" autocheck="false"/> + <menuitem id="history" label="Show History" type="checkbox" autocheck="false" checked="true"/> + <menuseparator/> + <menuitem id="byname" label="By Name" type="radio" name="sort"/> + <menuitem id="bydate" label="By Date" type="radio" name="sort" checked="true"/> + <menuseparator/> + <menuitem id="ascending" label="Ascending" type="radio" name="order" checked="true"/> + <menuitem id="descending" label="Descending" type="radio" name="order" autocheck="false"/> + <menuitem id="bysubject" label="By Subject" type="radio" name="sort"/> + </menupopup> + </button> + + </hbox> + + <!-- + This test checks that checkbox and radio menu items work properly + --> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + var gTestIndex = 0; + + // tests to perform + var tests = [ + { + testname: "select unchecked checkbox", + item: "toolbar", + checked: ["toolbar", "statusbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked checkbox", + item: "statusbar", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked autocheck checkbox", + item: "bookmarks", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select checked autocheck checkbox", + item: "history", + checked: ["toolbar", "history", "bydate", "ascending"] + }, + { + testname: "select unchecked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select checked radio", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select out of order checked radio", + item: "bysubject", + scroll: true, + checked: ["toolbar", "history", "bysubject", "ascending"] + }, + { + testname: "select first radio again", + item: "byname", + checked: ["toolbar", "history", "byname", "ascending"] + }, + { + testname: "select autocheck radio", + item: "descending", + checked: ["toolbar", "history", "byname", "ascending"] + } + ]; + + function runTest() + { + checkMenus(["statusbar", "history", "bydate", "ascending"], "initial"); + document.getElementById("menu").open = true; + } + + function checkMenus(checkedItems, testname) + { + var isok = true; + var children = document.getElementById("popup").childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ((checkedItems.includes(child.id) && child.getAttribute("checked") != "true") || + (!checkedItems.includes(child.id) && child.hasAttribute("checked"))) { + isok = false; + break; + } + } + + ok(isok, testname); + } + + function popupShown() + { + var test = tests[gTestIndex]; + + if (test.scroll) { + // On Windows 10, the menu is larger than the test frame. Scroll the later + // items into view. Since we are just testing the checked state of the items, + // and not their positions, this doesn't affect the behaviour of the test. + document.getElementById(test.item).scrollIntoView({ block: 'nearest' }); + } + synthesizeMouse(document.getElementById(test.item), 4, 4, { }); + } + + function popupHidden() + { + if (gTestIndex < tests.length) { + var test = tests[gTestIndex]; + checkMenus(test.checked, test.testname); + gTestIndex++; + if (gTestIndex < tests.length) { + document.getElementById("menu").open = true; + } + else { + // manually setting the checkbox should also update the radio state + document.getElementById("bydate").setAttribute("checked", "true"); + checkMenus(["toolbar", "history", "bydate", "ascending"], "set checked attribute on radio"); + SimpleTest.finish(); + } + } + } + + ]]> + </script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_blink.xhtml b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml new file mode 100644 index 0000000000..700a8a7465 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_blink.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Blinking Context Menu Item Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <menulist id="menulist"> + <menupopup id="menupopup"> + <menuitem label="Menu Item" id="menuitem"/> + </menupopup> + </menulist> +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest); + +function startTest() { + if (!/Mac/.test(navigator.platform)) { + ok(true, "Nothing to test on non-Mac."); + SimpleTest.finish(); + return; + } + test_crash(); +} + +function test_crash() { + var menupopup = document.getElementById("menupopup"); + var menuitem = document.getElementById("menuitem"); + menupopup.addEventListener("popupshown", function () { + menuitem.addEventListener("mouseup", function (e) { + const observer = new MutationObserver((aMutationList, aObserver) => { + for (const mutation of aMutationList) { + if (mutation.attributeName != "_moz-menuactive") { + continue; + } + menuitem.hidden = true; + menuitem.getBoundingClientRect(); + ok(true, "Didn't crash on _moz-menuactive mutation") + menuitem.hidden = false; + aObserver.disconnect(); + SimpleTest.executeSoon(function () { + menupopup.hidePopup(); + }); + } + }); + observer.observe(menuitem, { attributes: true }); + }, {once: true}); + menupopup.addEventListener("popuphidden", SimpleTest.finish, {once: true}); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousemove" }); + synthesizeMouse(menuitem, 10, 5, { type : "mousedown" }); + SimpleTest.executeSoon(function () { + synthesizeMouse(menuitem, 10, 5, { type : "mouseup" }); + }); + }, {once: true}); + document.getElementById("menulist").open = true; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menuitem_commands.xhtml b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml new file mode 100644 index 0000000000..60a35b36c5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menuitem_commands.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menuitem Commands Test" + onload="runOrOpen()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function checkAttributes(elem, label, accesskey, disabled, hidden, isAfter) +{ + var is = window.arguments[0].SimpleTest.is; + + is(elem.getAttribute("label"), label, elem.id + " label " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("accesskey"), accesskey, elem.id + " accesskey " + (isAfter ? "after" : "before") + " open"); + is(elem.getAttribute("disabled"), disabled, elem.id + " disabled " + (isAfter ? "after" : "before") + " open"); + is(elem.hidden, hidden, elem.id + " hidden " + (isAfter ? "after" : "before") + " open"); +} + +function runOrOpen() +{ + if (window.arguments) { + SimpleTest.waitForFocus(runTest); + } + else { + window.openDialog("test_menuitem_commands.xhtml", "", "chrome,noopener", window); + } +} + +function runTest() +{ + runTestSet(""); + runTestSet("bar"); + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function runTestSet(suffix) +{ + var isMac = (navigator.platform.includes("Mac")); + + var one = $("one" + suffix); + var two = $("two" + suffix); + var three = $("three" + suffix); + var four = $("four" + suffix); + + checkAttributes(one, "One", "", "", true, false); + checkAttributes(two, "", "", "false", false, false); + checkAttributes(three, "Three", "T", "true", false, false); + checkAttributes(four, "Four", "F", "", false, false); + + if (isMac && suffix) { + var utils = window.windowUtils; + utils.forceUpdateNativeMenuAt("0"); + } + else { + $("menu" + suffix).open = true; + } + + checkAttributes(one, "One", "", "", false, true); + checkAttributes(two, "Cat", "C", "", false, true); + checkAttributes(three, "Dog", "D", "false", true, true); + checkAttributes(four, "Four", "F", "true", false, true); + + $("menu" + suffix).open = false; +} +]]> +</script> + +<command id="cmd_one" hidden="false"/> +<command id="cmd_two" label="Cat" accesskey="C"/> +<command id="cmd_three" label="Dog" accesskey="D" disabled="false" hidden="true"/> +<command id="cmd_four" disabled="true"/> + +<button id="menu" type="menu"> + <menupopup> + <menuitem id="one" label="One" hidden="true" command="cmd_one"/> + <menuitem id="two" disabled="false" command="cmd_two"/> + <menuitem id="three" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="four" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> +</button> + +<menubar> + <menu id="menubar" label="Sample"> + <menupopup> + <menuitem id="onebar" label="One" hidden="true" command="cmd_one"/> + <menuitem id="twobar" disabled="false" command="cmd_two"/> + <menuitem id="threebar" label="Three" accesskey="T" disabled="true" command="cmd_three"/> + <menuitem id="fourbar" label="Four" accesskey="F" command="cmd_four"/> + </menupopup> + </menu> +</menubar> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist.xhtml b/toolkit/content/tests/chrome/test_menulist.xhtml new file mode 100644 index 0000000000..5917785b56 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist.xhtml @@ -0,0 +1,350 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(testtag_menulists, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"></script> + +<vbox id="scroller" style="overflow: auto; height: 60px"> + <menulist id="menulist" onpopupshown="test_menulist_open(this, this.parentNode)" + onpopuphidden="$('menulist-in-listbox').open = true;"> + <menupopup id="menulist-popup"/> + </menulist> + <button label="Two"/> + <button label="Three"/> +</vbox> +<richlistbox id="scroller-in-listbox" style="overflow: auto; height: 60px"> + <richlistitem allowevents="true"> + <menulist id="menulist-in-listbox" onpopupshown="test_menulist_open(this, this.parentNode.parentNode)" + onpopuphidden="SimpleTest.executeSoon(() => checkScrollAndFinish().catch(ex => ok(false, ex)));"> + <menupopup id="menulist-in-listbox-popup"> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + </menupopup> + </menulist> + </richlistitem> + <richlistitem><label value="Two"/></richlistitem> + <richlistitem><label value="Three"/></richlistitem> + <richlistitem><label value="Four"/></richlistitem> + <richlistitem><label value="Five"/></richlistitem> + <richlistitem><label value="Six"/></richlistitem> +</richlistbox> + +<hbox> + <menulist id="menulist-size"> + <menupopup> + <menuitem label="Menuitem Label" style="width: 200px"/> + </menupopup> + </menulist> +</hbox> + +<menulist id="menulist-initwithvalue" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + </menupopup> +</menulist> +<menulist id="menulist-initwithselected" value="two"> + <menupopup> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three" selected="true"/> + </menupopup> +</menulist> + +<menulist id="menulist-clipped"> + <menupopup style="height: 65px"> + <menuitem label="One" value="one"/> + <menuitem label="Two" value="two"/> + <menuitem label="Three" value="three"/> + <menuitem label="Four" value="four"/> + <menuitem label="Five" value="five" selected="true"/> + <menuitem label="Six" value="six"/> + <menuitem label="Seven" value="seven"/> + <menuitem label="Eight" value="eight"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function waitForEvent(subject, eventName, checkFn) { + return new Promise(resolve => { + subject.addEventListener(eventName, function listener(event) { + if (checkFn && !checkFn(event)) { + return; + } + subject.removeEventListener(eventName, listener); + SimpleTest.executeSoon(() => resolve(event)); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); + +function testtag_menulists() +{ + testtag_menulist_UI_start($("menulist"), false); +} + +function testtag_menulist_UI_start(element) +{ + // check the menupopup property + var popup = element.menupopup; + ok(popup && popup.localName == "menupopup" && + popup.parentNode == element, "menupopup"); + + // test the interfaces that menulist implements + test_nsIDOMXULMenuListElement(element); +} + +function testtag_menulist_UI_finish(element) +{ + element.value = ""; + + test_nsIDOMXULSelectControlElement(element, "menuitem", null); + + $("menulist").open = true; +} + +function test_nsIDOMXULMenuListElement(element) +{ + is(element.open, false, "open"); + + element.appendItem("Item One", "one"); + var seconditem = element.appendItem("Item Two", "two"); + var thirditem = element.appendItem("Item Three", "three"); + element.appendItem("Item Four", "four"); + + seconditem.image = "happy.png"; + seconditem.setAttribute("description", "This is the second description"); + thirditem.image = "happy.png"; + thirditem.setAttribute("description", "This is the third description"); + + // check the image and description properties + element.selectedIndex = 1; + is(element.image, "happy.png", "image set to selected"); + is(element.description, "This is the second description", "description set to selected"); + element.selectedIndex = -1; + is(element.image, "", "image set when none selected"); + is(element.description, "", "description set when none selected"); + element.selectedIndex = 2; + is(element.image, "happy.png", "image set to selected again"); + is(element.description, "This is the third description", "description set to selected again"); + + // check that changing the properties of the selected item changes the menulist's properties + let properties = [{attr: "label", value: "Item Number Three"}, + {attr: "value", value: "item-three"}, + {attr: "image", value: "smile.png"}, + {attr: "description", value: "Changed description"}]; + test_nsIDOMXULMenuListElement_properties(element, thirditem, properties); +} + +function test_nsIDOMXULMenuListElement_properties(element, thirditem, properties) +{ + let {attr, value} = properties.shift(); + let last = !properties.length; + + let mutObserver = new MutationObserver(() => { + is(element.getAttribute(attr), value, `${attr} modified`); + done(); + }); + mutObserver.observe(element, { attributeFilter: [attr] }); + + let failureTimeout = setTimeout(() => { + ok(false, `${attr} should have updated`); + done(); + }, 2000); + + function done() + { + clearTimeout(failureTimeout); + mutObserver.disconnect(); + if (!last) { + test_nsIDOMXULMenuListElement_properties(element, thirditem, properties); + } + else { + test_nsIDOMXULMenuListElement_unselected(element, thirditem); + } + } + + thirditem.setAttribute(attr, value) +} + +function test_nsIDOMXULMenuListElement_unselected(element, thirditem) +{ + let seconditem = thirditem.previousElementSibling; + seconditem.label = "Changed Label 2"; + is(element.label, "Item Number Three", "label of another item modified"); + + element.selectedIndex = 0; + is(element.image, "", "image set to selected with no image"); + is(element.description, "", "description set to selected with no description"); + test_nsIDOMXULMenuListElement_finish(element); +} + +function test_nsIDOMXULMenuListElement_finish(element) +{ + // check the removeAllItems method + element.appendItem("An Item", "anitem"); + element.appendItem("Another Item", "anotheritem"); + element.removeAllItems(); + is(element.itemCount, 0, "removeAllItems"); + + testtag_menulist_UI_finish(element); +} + +function test_menulist_open(element, scroller) +{ + element.appendItem("Scroll Item 1", "scrollitem1"); + element.appendItem("Scroll Item 2", "scrollitem2"); + element.focus(); + element.selectedIndex = 0; + +/* + // bug 530504, mousewheel while menulist is open should not scroll menulist + // items or parent + var scrolled = false; + var mouseScrolled = function (event) { scrolled = true; } + window.addEventListener("DOMMouseScroll", mouseScrolled, false); + synthesizeWheel(element, 2, 2, { deltaY: 10, + deltaMode: WheelEvent.DOM_DELTA_LINE }); + is(scrolled, true, "mousescroll " + element.id); + is(scroller.scrollTop, 0, "scroll position on mousescroll " + element.id); + window.removeEventListener("DOMMouseScroll", mouseScrolled, false); +*/ + + // bug 543065, hovering the mouse over an item should highlight it, not + // scroll the parent, and not change the selected index. + var item = element.menupopup.childNodes[1]; + + synthesizeMouse(element.menupopup.childNodes[1], 2, 2, { type: "mousemove" }); + synthesizeMouse(element.menupopup.childNodes[1], 6, 6, { type: "mousemove" }); + is(element.activeChild, item, "activeChild after menu highlight " + element.id); + is(element.selectedIndex, 0, "selectedIndex after menu highlight " + element.id); + is(scroller.scrollTop, 0, "scroll position after menu highlight " + element.id); + + element.open = false; +} + +async function checkScrollAndFinish() +{ + is($("scroller").scrollTop, 0, "mousewheel on menulist does not scroll vbox parent"); + is($("scroller-in-listbox").scrollTop, 0, "mousewheel on menulist does not scroll listbox parent"); + + let menulist = $("menulist-size"); + let shownPromise = waitForEvent(menulist, "popupshown"); + menulist.open = true; + await shownPromise; + + sendKey("ALT"); + is(menulist.menupopup.state, "open", "alt doesn't close menulist"); + menulist.open = false; + + await dragScroll(); +} + +async function dragScroll() +{ + let menulist = $("menulist-clipped"); + + let shownPromise = waitForEvent(menulist, "popupshown"); + menulist.open = true; + await shownPromise; + + let popup = menulist.menupopup; + let getScrollPos = () => popup.scrollBox.scrollbox.scrollTop; + let scrollPos = getScrollPos(); + let popupRect = popup.getBoundingClientRect(); + + // First, check that scrolling does not occur when the mouse is moved over the + // anchor button but not the popup yet. + synthesizeMouseAtPoint(popupRect.left + 5, popupRect.top - 10, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position after mousemove over button should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top + 10, { type: "mousemove" }); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top + 10, { type: "mousedown", buttons: 1 }); + + // Dragging above the popup scrolls it up. + let scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() < scrollPos - 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top - 20, { type: "mousemove", buttons: 1 }); + await scrolledPromise; + ok(true, "scroll position at drag up"); + + // Dragging below the popup scrolls it down. + scrollPos = getScrollPos(); + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() > scrollPos + 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove", buttons: 1 }); + await scrolledPromise; + ok(true, "scroll position at drag down"); + + // Releasing the mouse button and moving the mouse does not change the scroll position. + scrollPos = getScrollPos(); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 25, { type: "mouseup" }); + is(getScrollPos(), scrollPos, "scroll position at mouseup should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position at mousemove after mouseup should not change"); + + // Now check dragging with a mousedown on an item. Make sure the element is + // visible, as the asynchronous scrolling may have moved it out of view. + popup.childNodes[4].scrollIntoView({ block: "nearest", behavior: "instant" }); + let menuRect = popup.childNodes[4].getBoundingClientRect(); + synthesizeMouseAtPoint(menuRect.left + 5, menuRect.top + 5, { type: "mousedown", buttons: 1 }); + + // Dragging below the popup scrolls it down. + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() > scrollPos + 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove", buttons: 1 }); + await scrolledPromise; + ok(true, "scroll position at drag down from item"); + + // Dragging above the popup scrolls it up. + scrollPos = getScrollPos(); + scrolledPromise = waitForEvent(popup, "scroll", false, + () => getScrollPos() < scrollPos - 5); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.top - 20, { type: "mousemove", buttons: 1 }); + await scrolledPromise; + ok(true, "scroll position at drag up from item"); + + scrollPos = getScrollPos(); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 25, { type: "mouseup" }); + is(getScrollPos(), scrollPos, "scroll position at mouseup should not change"); + + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + is(getScrollPos(), scrollPos, "scroll position at mousemove after mouseup should not change"); + + menulist.open = false; + + let mouseMoveTarget = null; + popup.childNodes[4].click(); + addEventListener("mousemove", function checkMouseMove(event) { + mouseMoveTarget = event.target; + }, {once: true}); + synthesizeMouseAtPoint(popupRect.left + 20, popupRect.bottom + 20, { type: "mousemove" }); + isnot(mouseMoveTarget, popup, "clicking on item when popup closed doesn't start dragging"); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml b/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml new file mode 100644 index 0000000000..971fe90322 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_in_popup.xhtml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist position Test" + onload="setTimeout(runTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +async function runTest() { + let panel = document.querySelector("panel"); + let menulist = document.getElementById("menulist"); + let menulistPopup = document.getElementById("menulistpopup"); + + menulistPopup.addEventListener("popupshown", function(e) { + ok(false, "Menulist popup shown"); + }); + + let panelShown = new Promise(r => panel.addEventListener("popupshown", r, { once: true })); + info("opening panel"); + panel.openPopup(null, { x: 0, y: 0 }); + await panelShown; + info("panel opened"); + + info("hovering menulist"); + synthesizeMouseAtCenter(menulist, { type: "mousemove" }); + info("waiting for a bit"); + await new Promise(r => setTimeout(r, 500)); + + isnot(menulist.open, "menulist should not be open on hover when inside a popup"); + + SimpleTest.finish(); +} + +]]> +</script> + +<panel style="width: 500px; height: 500px"> + <menulist style="width: 200px" id="menulist"> + <menupopup style="max-height: 90px;" id="menulistpopup"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + </menupopup> + </menulist> +</panel> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_keynav.xhtml b/toolkit/content/tests/chrome/test_menulist_keynav.xhtml new file mode 100644 index 0000000000..86e86b6510 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_keynav.xhtml @@ -0,0 +1,316 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Key Navigation Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<button id="button1" label="One"/> +<menulist id="list"> + <menupopup id="popup" onpopupshowing="return gShowPopup;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i2b" disabled="true" label="Two and a Half"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> + </menupopup> +</menulist> +<button id="button2" label="Two"/> +<menulist id="list2"> + <menupopup id="popup" onpopupshown="checkCursorNavigation();"> + <menuitem id="b1" label="One"/> + <menuitem id="b2" label="Two" selected="true"/> + <menuitem id="b3" label="Three"/> + <menuitem id="b4" label="Four"/> + </menupopup> +</menulist> +<menulist id="list3" sizetopopup="none"> + <menupopup> + <menuitem id="s1" label="One"/> + <menuitem id="s2" label="Two"/> + <menuitem id="s3" label="Three"/> + <menuitem id="s4" label="Four"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gShowPopup = false; +var gModifiers = 0; +var gOpenPhase = false; + +var list = $("list"); +let expectCommandEvent; + +var iswin = (navigator.platform.indexOf("Win") == 0); +var ismac = (navigator.platform.indexOf("Mac") == 0); + +function runTests() +{ + list.focus(); + + // on Mac, up and cursor keys open the menu, but on other platforms, the + // cursor keys navigate between items without opening the menu + if (!ismac) { + expectCommandEvent = true; + keyCheck(list, "KEY_ArrowDown", 2, 1, "cursor down"); + keyCheck(list, "KEY_ArrowDown", 3, 1, "cursor down skip disabled"); + keyCheck(list, "KEY_ArrowUp", 2, 1, "cursor up skip disabled"); + keyCheck(list, "KEY_ArrowUp", 1, 1, "cursor up"); + + // On Windows, wrapping doesn't occur. + expectCommandEvent = !iswin; + keyCheck(list, "KEY_ArrowUp", iswin ? 1 : 4, 1, "cursor up wrap"); + + list.selectedIndex = 4; + list.activeChild = list.selectedItem; + keyCheck(list, "KEY_ArrowDown", iswin ? 4 : 1, 4, "cursor down wrap"); + + list.selectedIndex = 0; + list.activeChild = list.selectedItem; + } + + // check that attempting to open the menulist does not change the selection + synthesizeKey("KEY_ArrowDown", {altKey: !ismac}); + is(list.selectedItem, $("i1"), "open menulist down selectedItem"); + synthesizeKey("KEY_ArrowUp", {altKey: !ismac}); + is(list.selectedItem, $("i1"), "open menulist up selectedItem"); + + list.selectedItem = $("i1"); + + pressLetter(); +} + +function pressLetter() +{ + // A command event should be fired only if the menulist is closed. + expectCommandEvent = !gOpenPhase || iswin; + + sendString("G"); + is(list.selectedItem, $("i1"), "letter pressed not found selectedItem"); + + keyCheck(list, "T", 2, 1, "letter pressed"); + + setTimeout(pressedAgain, 1200); +} + +function pressedAgain() +{ + keyCheck(list, "T", 3, 1, "letter pressed again"); + SpecialPowers.setIntPref("ui.menu.incremental_search.timeout", 0); // prevent to timeout + keyCheck(list, "W", 2, 1, "second letter pressed"); + SpecialPowers.clearUserPref("ui.menu.incremental_search.timeout"); + setTimeout(differentPressed, 1200); +} + +function differentPressed() +{ + keyCheck(list, "O", 1, 1, "different letter pressed"); + + if (gOpenPhase) { + list.open = false; + tabAndScroll(); + } + else { + // Run the letter tests again with the popup open + info("list open phase"); + + list.selectedItem = $("i1"); + + // Hide and show the list to avoid using any existing incremental key state. + list.hidden = true; + list.clientWidth; + list.hidden = false; + + gShowPopup = true; + gOpenPhase = true; + + list.addEventListener("popupshown", function popupShownListener() { + pressLetter(); + }, { once: true}); + + list.open = true; + } +} + +function inputMargin(el) { + let cs = getComputedStyle(el); + // XXX Internal properties are not exposed in getComputedStyle, so we have to + // use margin and rely on our knowledge of them matching negative margins + // where appropriate. + // return parseFloat(cs.getPropertyValue("-moz-window-input-region-margin")); + return ismac ? 0 : Math.max(-parseFloat(cs.marginLeft), 0); +} + +function tabAndScroll() +{ + list = $("list"); + + if (!ismac) { + $("button1").focus(); + synthesizeKeyExpectEvent("KEY_Tab", {}, list, "focus", "focus to menulist"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("button2"), "focus", "focus to button"); + is(document.activeElement, $("button2"), "tab from menulist focused button"); + } + + // now make sure that using a key scrolls the menu correctly + + for (let i = 0; i < 65; i++) { + list.appendItem("Item" + i, "item" + i); + } + list.open = true; + is(list.getBoundingClientRect().width, list.menupopup.getBoundingClientRect().width - 2 * inputMargin(list.menupopup), + "menu and popup width match"); + var minScrollbarWidth = window.matchMedia("(-moz-overlay-scrollbars)").matches ? 0 : 3; + ok(list.getBoundingClientRect().width >= list.getItemAtIndex(0).getBoundingClientRect().width + minScrollbarWidth, + "menuitem width accounts for scrollbar"); + list.open = false; + + list.menupopup.style.maxHeight = "100px"; + list.open = true; + + var rowdiff = list.getItemAtIndex(1).getBoundingClientRect().top - + list.getItemAtIndex(0).getBoundingClientRect().top; + + var item = list.getItemAtIndex(10); + var originalPosition = item.getBoundingClientRect().top; + + list.activeChild = item; + ok(item.getBoundingClientRect().top < originalPosition, + "position of item 1: " + item.getBoundingClientRect().top + " -> " + originalPosition); + + originalPosition = item.getBoundingClientRect().top; + + synthesizeKey("KEY_ArrowDown"); + is(item.getBoundingClientRect().top, originalPosition - rowdiff, "position of item 10"); + + list.open = false; + + checkEnter(); +} + +function keyCheck(list, key, index, defaultindex, testname) +{ + info(`keyCheck(${index}, ${key}, ${index}, ${defaultindex}, ${testname}, ${expectCommandEvent})`); + var item = $("i" + index); + var defaultitem = $("i" + defaultindex || 1); + + synthesizeKeyExpectEvent(key, { }, item, expectCommandEvent ? "command" : "!command", testname); + is(list.selectedItem, expectCommandEvent ? item : defaultitem, testname + " selectedItem----" + list.selectedItem.id); +} + +function checkModifiers(event) +{ + var expectedModifiers = (gModifiers == 1); + is(event.shiftKey, expectedModifiers, "shift key pressed"); + is(event.ctrlKey, expectedModifiers, "ctrl key pressed"); + is(event.altKey, expectedModifiers, "alt key pressed"); + is(event.metaKey, expectedModifiers, "meta key pressed"); + gModifiers++; +} + +function checkEnter() +{ + list.addEventListener("popuphidden", checkEnterWithModifiers); + list.addEventListener("command", checkModifiers); + list.open = true; + synthesizeKey("KEY_Enter"); +} + +function checkEnterWithModifiers() +{ + is(gModifiers, 1, "modifiers checked when not set"); + + ok(!list.open, "list closed on enter press"); + list.removeEventListener("popuphidden", checkEnterWithModifiers); + + list.addEventListener("popuphidden", verifyPopupOnClose); + list.open = true; + + synthesizeKey("KEY_Enter", {shiftKey: true, ctrlKey: true, altKey: true, metaKey: true}); +} + +function verifyPopupOnClose() +{ + is(gModifiers, 2, "modifiers checked when set"); + + ok(!list.open, "list closed on enter press with modifiers"); + list.removeEventListener("popuphidden", verifyPopupOnClose); + + list = $("list2"); + list.focus(); + list.open = true; +} + +function checkCursorNavigation() +{ + var commandEventsCount = 0; + list.addEventListener("command", event => { + is(event.target, list.selectedItem, "command event fired on selected item"); + commandEventsCount++; + }); + + is(list.selectedIndex, 1, "selectedIndex before cursor down"); + synthesizeKey("KEY_ArrowDown"); + is(list.selectedIndex, iswin ? 2 : 1, "selectedIndex after cursor down"); + is(commandEventsCount, iswin ? 1 : 0, "selectedIndex after cursor down command event"); + is(list.menupopup.state, "open", "cursor down popup state"); + synthesizeKey("KEY_PageDown"); + is(list.selectedIndex, iswin ? 2 : 1, "selectedIndex after page down"); + is(commandEventsCount, iswin ? 1 : 0, "selectedIndex after page down command event"); + is(list.menupopup.state, "open", "page down popup state"); + + // Check whether cursor up and down wraps. + list.selectedIndex = 0; + list.activeChild = list.selectedItem; + synthesizeKey("KEY_ArrowUp"); + is(list.activeChild, + document.getElementById(iswin || ismac ? "b1" : "b4"), "cursor up wrap while open"); + + list.selectedIndex = 3; + list.activeChild = list.selectedItem; + synthesizeKey("KEY_ArrowDown"); + is(list.activeChild, + document.getElementById(iswin || ismac ? "b4" : "b1"), "cursor down wrap while open"); + + synthesizeKey("KEY_ArrowUp", {altKey: true}); + is(list.open, ismac, "alt+up closes popup"); + + if (ismac) { + list.open = false; + } + + // Finally, test a menulist with sizetopopup="none" to ensure keyboard navigation + // still works when the popup has not been opened. + if (!ismac) { + let unsizedMenulist = document.getElementById("list3"); + unsizedMenulist.focus(); + synthesizeKey("KEY_ArrowDown"); + is(unsizedMenulist.selectedIndex, 1, "correct menulist index on keydown"); + is(unsizedMenulist.label, "Two", "correct menulist label on keydown"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_null_value.xhtml b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml new file mode 100644 index 0000000000..9312c236dc --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_null_value.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist value property" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menulist id="list"> + <menupopup> + <menuitem id="i0" label="Zero" value="0"/> + <menuitem id="i1" label="One" value="item1"/> + <menuitem id="i2" label="Two" value="item2"/> + <menuitem id="ifalse" label="False" value="false"/> + <menuitem id="iempty" label="Empty" value=""/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + var list = document.getElementById("list"); + + list.value = "item2"; + is(list.value, "item2", "Check list value after setting value"); + is(list.getAttribute("label"), "Two", "Check list label after setting value"); + + list.selectedItem = null; + is(list.value, "", "Check list value after setting selectedItem to null"); + is(list.getAttribute("label"), "", "Check list label after setting selectedItem to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + is(list.value, "item1", "Check list value after setting selectedIndex"); + is(list.getAttribute("label"), "One", "Check list label after setting selectedIndex"); + + // check that an item can have the "false" value + list.value = false; + is(list.value, "false", "Check list value after setting it to false"); + is(list.getAttribute("label"), "False", "Check list labem after setting value to false"); + + // check that an item can have the "0" value + list.value = 0; + is(list.value, "0", "Check list value after setting it to 0"); + is(list.getAttribute("label"), "Zero", "Check list label after setting value to 0"); + + // check that an item can have the empty string value. + list.value = ""; + is(list.value, "", "Check list value after setting it to an empty string"); + is(list.getAttribute("label"), "Empty", "Check list label after setting value to an empty string"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to null and test it (bug 408940) + list.value = null; + is(list.value, "", "Check list value after setting value to null"); + is(list.getAttribute("label"), "", "Check list label after setting value to null"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to undefined and test it (bug 408940) + list.value = undefined; + is(list.value, "", "Check list value after setting value to undefined"); + is(list.getAttribute("label"), "", "Check list label after setting value to undefined"); + + // select something again to make sure the label is not already empty + list.selectedIndex = 1; + // set the value to something that does not exist in any menuitem of the list + // and make sure the previous label is removed + list.value = "this does not exist"; + is(list.value, "this does not exist", "Check the list value after setting it to something not associated witn an existing menuitem"); + is(list.getAttribute("label"), "", "Check that the list label is empty after selecting a nonexistent item"); + + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_paging.xhtml b/toolkit/content/tests/chrome/test_menulist_paging.xhtml new file mode 100644 index 0000000000..7a0c6f3b5d --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_paging.xhtml @@ -0,0 +1,178 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist Tests" + onload="setTimeout(runTest, 0);" + onpopupshown="menulistShown()" onpopuphidden="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<menulist id="menulist1"> + <menupopup id="menulist-popup1"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist2"> + <menupopup id="menulist-popup2"> + <menuitem label="One" disabled="true"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <menuitem label="Ten" disabled="true"/> + </menupopup> +</menulist> + +<menulist id="menulist3"> + <menupopup id="menulist-popup3"> + <label value="One"/> + <menuitem label="Two" selected="true"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five" disabled="true"/> + <menuitem label="Six" disabled="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<menulist id="menulist4"> + <menupopup id="menulist-popup4"> + <label value="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six" selected="true"/> + <menuitem label="Seven"/> + <menuitem label="Eight"/> + <menuitem label="Nine"/> + <label value="Ten"/> + </menupopup> +</menulist> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +let test; + +// Fields: +// list - menulist id +// initial - initial selected index +// scroll - index of item at top of the visible scrolled area, -1 to skip this test +// downs - array of indicies that will be selected when pressing down in sequence +// ups - array of indicies that will be selected when pressing up in sequence +let tests = [ + { list: "menulist1", initial: 0, scroll: 0, downs: [3, 6, 9, 9], + ups: [6, 3, 0, 0] }, + { list: "menulist2", initial: 1, scroll: 0, downs: [4, 7, 8, 8], + ups: [5, 2, 1] }, + { list: "menulist3", initial: 1, scroll: -1, downs: [3, 6, 8, 8], + ups: [6, 3, 1] }, + { list: "menulist4", initial: 5, scroll: 2, downs: [], ups: [] } +]; + +let gMeasured = false; +function measureMenuItemHeightIfNeeded() { + if (gMeasured) { + return; + } + gMeasured = true; + + let popup = document.getElementById("menulist-popup1"); + let menuitemHeight = popup.firstChild.getBoundingClientRect().height; + + let cs = window.getComputedStyle(popup); + let csArrow = window.getComputedStyle(popup.scrollBox); + let bpmTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth) + + parseFloat(csArrow.paddingTop) + parseFloat(csArrow.borderTopWidth) + + parseFloat(csArrow.marginTop); + let bpmBottom = parseFloat(cs.paddingBottom) + parseFloat(cs.borderBottomWidth) + + parseFloat(csArrow.paddingBottom) + parseFloat(csArrow.borderBottomWidth) + + parseFloat(csArrow.marginBottom); + + // First, set the height of each popup to the height of four menuitems plus + // any padding / border / margin on the menupopup. + let height = menuitemHeight * 4 + bpmTop + bpmBottom; + + popup.style.height = height + "px"; + document.getElementById("menulist-popup2").style.height = height + "px"; + document.getElementById("menulist-popup3").style.height = height + "px"; + document.getElementById("menulist-popup4").style.height = height + "px"; +} + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + test = tests.shift(); + document.getElementById(test.list).open = true; +} + +function menulistShown() +{ + measureMenuItemHeightIfNeeded(); + + let menulist = document.getElementById(test.list); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.initial).label, test.list + " initial selection"); + + let cs = window.getComputedStyle(menulist.menupopup); + let csArrow = window.getComputedStyle(menulist.menupopup.scrollBox); + let bpmTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth) + + parseFloat(csArrow.paddingTop) + parseFloat(csArrow.borderTopWidth) + + parseFloat(csArrow.marginTop); + + // Skip menulist3 as it has a label that scrolling doesn't need normally deal with. + if (test.scroll >= 0) { + is(menulist.menupopup.childNodes[test.scroll].getBoundingClientRect().top, + menulist.menupopup.getBoundingClientRect().top + bpmTop, + "Popup scroll at correct position"); + } + + for (let i = 0; i < test.downs.length; i++) { + sendKey("PAGE_DOWN"); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.downs[i]).label, test.list + " page down " + i); + } + + for (let i = 0; i < test.ups.length; i++) { + sendKey("PAGE_UP"); + is(menulist.activeChild.label, menulist.getItemAtIndex(test.ups[i]).label, test.list + " page up " + i); + } + + menulist.open = false; +} +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_menulist_position.xhtml b/toolkit/content/tests/chrome/test_menulist_position.xhtml new file mode 100644 index 0000000000..b055c1cdb4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_menulist_position.xhtml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menulist position Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks the position of a menulist's popup. + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var menulist; + +function init() +{ + menulist = document.getElementById("menulist"); + menulist.open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +const ismac = navigator.platform.indexOf("Mac") == 0; +function inputMargin(el) { + let cs = getComputedStyle(el); + // XXX Internal properties are not exposed in getComputedStyle, so we have to + // use margin and rely on our knowledge of them matching negative margins + // where appropriate. + // return parseFloat(cs.getPropertyValue("-moz-window-input-region-margin")); + return ismac ? 0 : Math.max(-parseFloat(cs.marginLeft), 0); +} + +function popupShown() +{ + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), `left position: ${menurect.left}, ${popuprect.left}`); + ok(isWithinHalfPixel(menurect.right + marginLeft + 2 * inputMargin(menulist.menupopup), popuprect.right), `right position: ${menurect.right}, ${popuprect.right}`); + + let index = menulist.selectedIndex; + if (menulist.selectedItem && navigator.platform.includes("Mac")) { + let menulistlabelrect = menulist.shadowRoot.getElementById("label").getBoundingClientRect(); + let mitemlabelrect = menulist.selectedItem.querySelector(".menu-iconic-text").getBoundingClientRect(); + + ok(isWithinHalfPixel(menulistlabelrect.top, mitemlabelrect.top), + `Labels vertically aligned for ${index} : ${menulistlabelrect.top} vs. ${mitemlabelrect.top}`); + + // Store the current value and reset it afterwards. + let current = menulist.selectedIndex; + + // Cycle through the items to ensure that the popup doesn't move when the selection changes. + for (let i = 0; i < menulist.itemCount; i++) { + menulist.selectedIndex = i; + + let newpopuprect = menulist.menupopup.getBoundingClientRect(); + is(newpopuprect.x, popuprect.x, "Popup remained horizontally for index " + i + " starting at " + current); + is(newpopuprect.y, popuprect.y, "Popup remained vertically for index " + i + " starting at " + current); + } + menulist.selectedIndex = current; + } + else { + let marginTop = parseFloat(getComputedStyle(menulist.menupopup).marginTop); + ok(isWithinHalfPixel(menurect.bottom + marginTop, popuprect.top), + "Vertical alignment with no selection for index " + index); + } + + menulist.open = false; +} + +function popupHidden() +{ + if (!menulist.selectedItem) { + SimpleTest.finish(); + } + else { + menulist.selectedItem = menulist.selectedItem.nextSibling; + menulist.open = true; + } +} +]]> +</script> + +<hbox align="center" pack="center" style="margin-top: 140px;"> + <menulist style="width: 200px" id="menulist" onpopupshown="popupShown();" onpopuphidden="popupHidden();" native="true"> + <menupopup style="max-height: 90px;"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="Four"/> + <menuitem label="Five"/> + <menuitem label="Six"/> + <menuitem label="Seven"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_mousescroll.xhtml b/toolkit/content/tests/chrome/test_mousescroll.xhtml new file mode 100644 index 0000000000..875ca4ac98 --- /dev/null +++ b/toolkit/content/tests/chrome/test_mousescroll.xhtml @@ -0,0 +1,291 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=378028 +--> +<window title="Mozilla Bug 378028" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=378028" + target="_blank">Mozilla Bug 378028</a> + </body> + + <!-- richlistbox currently has no way of giving us a defined number of + rows, so we just choose an arbitrary height limit that should give + us plenty of vertical scrollability --> + <richlistbox id="richlistbox" style="height:50px;"> + <richlistitem id="richlistbox_item0" hidden="true"><label value="Item 0"/></richlistitem> + <richlistitem id="richlistbox_item1"><label value="Item 1"/></richlistitem> + <richlistitem id="richlistbox_item2"><label value="Item 2"/></richlistitem> + <richlistitem id="richlistbox_item3"><label value="Item 3"/></richlistitem> + <richlistitem id="richlistbox_item4"><label value="Item 4"/></richlistitem> + <richlistitem id="richlistbox_item5"><label value="Item 5"/></richlistitem> + <richlistitem id="richlistbox_item6"><label value="Item 6"/></richlistitem> + <richlistitem id="richlistbox_item7"><label value="Item 7"/></richlistitem> + <richlistitem id="richlistbox_item8"><label value="Item 8"/></richlistitem> + </richlistbox> + + <box orient="horizontal"> + <arrowscrollbox id="hscrollbox" clicktoscroll="true" orient="horizontal" + smoothscroll="false" style="max-width:80px;" flex="1"> + <hbox style="min-width:40px; min-height:20px; background:black;" hidden="true"/> + <hbox style="min-width:40px; min-height:20px; background:white;"/> + <hbox style="min-width:40px; min-height:20px; background:black;"/> + <hbox style="min-width:40px; min-height:20px; background:white;"/> + <hbox style="min-width:40px; min-height:20px; background:black;"/> + <hbox style="min-width:40px; min-height:20px; background:white;"/> + <hbox style="min-width:40px; min-height:20px; background:black;"/> + <hbox style="min-width:40px; min-height:20px; background:white;"/> + <hbox style="min-width:40px; min-height:20px; background:black;"/> + </arrowscrollbox> + </box> + + <arrowscrollbox id="vscrollbox" clicktoscroll="true" orient="vertical" + smoothscroll="false" style="max-height:80px;" flex="1"> + <vbox style="min-width:100px; min-height:40px; background:black;" hidden="true"/> + <vbox style="min-width:100px; min-height:40px; background:white;"/> + <vbox style="min-width:100px; min-height:40px; background:black;"/> + <vbox style="min-width:100px; min-height:40px; background:white;"/> + <vbox style="min-width:100px; min-height:40px; background:black;"/> + <vbox style="min-width:100px; min-height:40px; background:white;"/> + <vbox style="min-width:100px; min-height:40px; background:black;"/> + <vbox style="min-width:100px; min-height:40px; background:white;"/> + <vbox style="min-width:100px; min-height:40px; background:black;"/> + <vbox style="min-width:100px; min-height:40px; background:white;"/> + <vbox style="min-width:100px; min-height:40px; background:black;"/> + </arrowscrollbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Bug 378028 **/ +/* and for Bug 350471 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(prepareRunningTests); + +// Some tests need to wait until stopping scroll completely. At this time, +// setTimeout() will retry to check up to MAX_RETRY_COUNT times. +const MAX_RETRY_COUNT = 5; + +const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE // 2 +]; + +function sendWheelAndWait(aScrollTaget, aX, aY, aEvent, aChecker) +{ + function continueTestsIfScrolledAsExpected() { + if (!aChecker()) + SimpleTest.executeSoon(()=>{ continueTestsIfScrolledAsExpected(aChecker) }); + else + runTests(); + } + + sendWheelAndPaint(aScrollTaget, aX, aY, aEvent, ()=>{ + // sendWheelAndPaint may wait not enough for <scrollbox>. + // Let's check the position before using is() for avoiding random orange. + // So, this test may detect regressions with timeout. + continueTestsIfScrolledAsExpected(aChecker); + }); +} + +function* testRichListbox(id) +{ + var listbox = document.getElementById(id); + + function* helper(aStart, aDelta, aIntDelta, aDeltaMode) { + listbox.ensureElementIsVisible(listbox.getItemAtIndex(aStart),true); + + let event = { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta + }; + // We don't need to wait for finishing the scroll in this test. + yield sendWheelAndWait(listbox, 10, 10, event, ()=>{ return true; }); + var change = listbox.getIndexOfFirstVisibleRow() - aStart; + var direction = (change > 0) - (change < 0); + var expected = (aDelta > 0) - (aDelta < 0); + is(direction, expected, + "testRichListbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaY " + aIntDelta + + " aDeltaMode " + aDeltaMode); + + // Check that horizontal scrolling has no effect + event = { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta + }; + + listbox.ensureElementIsVisible(listbox.getItemAtIndex(aStart),true); + yield sendWheelAndWait(listbox, 10, 10, event, ()=>{ return true; }); + is(listbox.getIndexOfFirstVisibleRow(), aStart, + "testRichListbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDeltaX " + aIntDelta + + " aDeltaMode " + aDeltaMode); + } + + // richlistbox currently uses native XUL scrolling, so the "line" + // amounts don't necessarily correspond 1-to-1 with listbox items. So + // we just check that scrolling up/down scrolls in the right direction. + for (let i = 0; i < deltaModes.length; i++) { + let delta = (deltaModes[i] == WheelEvent.DOM_DELTA_PIXEL) ? 32.0 : 2.0; + yield* helper(5, -delta, -1, deltaModes[i]); + yield* helper(5, -delta, 0, deltaModes[i]); + yield* helper(5, delta, 1, deltaModes[i]); + yield* helper(5, delta, 0, deltaModes[i]); + } +} + +function* testArrowScrollbox(id) +{ + var arrowscrollbox = document.getElementById(id); + var scrollbox = arrowscrollbox.scrollbox; + var orient = scrollbox.getAttribute("orient"); + var orientIsHorizontal = (orient == "horizontal"); + + function* helper(aStart, aDelta, aDeltaMode, aExpected) + { + var lineOrPageDelta = (aDeltaMode == WheelEvent.DOM_DELTA_PIXEL) ? aDelta / 10 : aDelta; + + scrollbox.scrollTo(aStart, aStart); + for (let i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + // Note, vertical mouse scrolling is allowed to scroll horizontal + // arrowscrollboxes, because many users have no horizontal mouse scroll + // capability + let expected = !i ? aExpected : aStart; + let getPos = ()=>{ + return orientIsHorizontal ? scrollbox.scrollLeft : + scrollbox.scrollTop; + }; + let oldPos = -1; + let retry = 0; + yield sendWheelAndWait(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaY: aDelta, + lineOrPageDeltaY: lineOrPageDelta }, + ()=>{ + if (getPos() == expected) { + return true; + } + if (oldPos == getPos()) { + // If scroll stopped completely, let's continue the test. + return ++retry == MAX_RETRY_COUNT; + } + oldPos = getPos(); + retry = 0; + return false; + }); + is(getPos(), expected, + "testArrowScrollbox(" + id + "): vertical, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + + scrollbox.scrollTo(aStart, aStart); + for (let i = orientIsHorizontal ? 2 : 0; i >= 0; i--) { + // horizontal mouse scrolling is never allowed to scroll vertical + // arrowscrollboxes + let expected = (!i && orientIsHorizontal) ? aExpected : aStart; + let getPos = ()=>{ + return orientIsHorizontal ? scrollbox.scrollLeft : + scrollbox.scrollTop; + }; + let oldPos = -1; + let retry = 0; + yield sendWheelAndWait(scrollbox, 5, 5, + { deltaMode: aDeltaMode, deltaX: aDelta, + lineOrPageDeltaX: lineOrPageDelta }, + ()=>{ + if (getPos() == expected) { + return true; + } + if (oldPos == getPos()) { + // If scroll stopped completely, let's continue the test. + return ++retry == MAX_RETRY_COUNT; + } + oldPos = getPos(); + retry = 0; + return false; + }); + is(getPos(), expected, + "testArrowScrollbox(" + id + "): horizontal, starting " + aStart + + " delta " + aDelta + " lineOrPageDelta " + lineOrPageDelta + + " aDeltaMode " + aDeltaMode); + } + } + + var line = arrowscrollbox.lineScrollAmount; + var scrolledWidth = scrollbox.scrollWidth; + var scrolledHeight = scrollbox.scrollHeight; + var scrollMaxX = scrolledWidth - scrollbox.getBoundingClientRect().width; + var scrollMaxY = scrolledHeight - scrollbox.getBoundingClientRect().height; + var scrollMax = orientIsHorizontal ? scrollMaxX : scrollMaxY; + + for (let deltaMode of deltaModes) { + const start = 50; + const delta = 1000; + let expectedNegative = 0; + let expectedPositive = scrollMax; + if (deltaMode == WheelEvent.DOM_DELTA_LINE) { + let maxDelta = Math.floor(Math.max(1, arrowscrollbox.scrollClientSize / line)) * line; + expectedNegative = Math.max(0, start - maxDelta); + expectedPositive = Math.min(scrollMax, start + maxDelta); + } + yield* helper(start, -delta, deltaMode, expectedNegative); + yield* helper(start, delta, deltaMode, expectedPositive); + } +} + +var gTestContinuation = null; + +function runTests() +{ + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + var winUtils = SpecialPowers.getDOMWindowUtils(window); + winUtils.restoreNormalRefresh(); + SimpleTest.finish(); + } +} + +async function prepareRunningTests() +{ + // Before actually running tests, we disable auto-dir scrolling, becasue the + // horizontal scrolling tests in this file are mostly meant to ensure that the + // tested controls in the default style should only have one scrollbar and it + // must always be in the block-flow direction so they are not really meant to + // test default actions for wheel events, so we simply disabled auto-dir + // scrolling, which are well tested in + // dom/events/test/window_wheel_default_action.html. + await SpecialPowers.pushPrefEnv({"set": [["mousewheel.autodir.enabled", + false]]}); + + runTests(); +} + +function* testBody() +{ + yield* testRichListbox("richlistbox"); + + // Perform a mousedown to ensure the wheel transaction from the previous test + // does not impact the next test. + synthesizeMouse(document.scrollingElement, 0, 0, {type: "mousedown"}, window); + yield* testArrowScrollbox("hscrollbox"); + + synthesizeMouse(document.scrollingElement, -1, -1, {type: "mousedown"}, window); + yield* testArrowScrollbox("vscrollbox"); +} + + ]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml b/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml new file mode 100644 index 0000000000..c2ed8a4787 --- /dev/null +++ b/toolkit/content/tests/chrome/test_mozinputbox_dictionary.xhtml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for textbox with Add and Undo Add to Dictionary + --> +<window title="Textbox Add and Undo Add to Dictionary Test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <hbox> + <moz-input-box id="t1" oncontextmenu="runContextMenuTest()" spellcheck="true"> + <html:input class="textbox-input" value="Hellop" spellcheck="true"/> + </moz-input-box> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var inputBox; +var testNum; + +function bringUpContextMenu(element) +{ + synthesizeMouseAtCenter(element, { type: "contextmenu", button: 2}); +} + +function leftClickElement(element) +{ + synthesizeMouseAtCenter(element, { button: 0 }); +} + +var onSpellCheck; +function startTests() +{ + inputBox = document.getElementById("t1"); + inputBox._input.focus(); + testNum = 0; + + ({onSpellCheck} = ChromeUtils.importESModule( + "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs")); + onSpellCheck(inputBox._input, function () { + bringUpContextMenu(inputBox); + }); +} + +function runContextMenuTest() +{ + SimpleTest.executeSoon(function() { + var contextMenu = inputBox.menupopup; + + switch(testNum) + { + case 0: // "Add to Dictionary" button + var addToDict = inputBox.getMenuItem("spell-add-to-dictionary"); + ok(!addToDict.hidden, "Is Add to Dictionary visible?"); + + var separator = inputBox.getMenuItem("spell-suggestions-separator"); + ok(!separator.hidden, "Is separator visible?"); + + addToDict.doCommand(); + + contextMenu.hidePopup(); + testNum++; + + onSpellCheck(inputBox._input, function () { + bringUpContextMenu(inputBox); + }); + break; + + case 1: // "Undo Add to Dictionary" button + var undoAddDict = inputBox.getMenuItem("spell-undo-add-to-dictionary"); + ok(!undoAddDict.hidden, "Is Undo Add to Dictioanry visible?"); + + separator = inputBox.getMenuItem("spell-suggestions-separator"); + ok(!separator.hidden, "Is separator hidden?"); + + undoAddDict.doCommand(); + + contextMenu.hidePopup(); + onSpellCheck(inputBox._input, function () { + SimpleTest.finish(); + }); + break; + } + }); +} + +SimpleTest.waitForFocus(startTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_named_deck.html b/toolkit/content/tests/chrome/test_named_deck.html new file mode 100644 index 0000000000..3445ef05c7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_named_deck.html @@ -0,0 +1,251 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <meta charset="utf-8"> + <title><!-- Shadow Parts issue with xul/xbl domparser --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script> +const { BrowserTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); +const DEFAULT_SECTION_NAMES = ["one", "two", "three"]; + +function makeButton({ name, deckId }) { + let button = document.createElement("button", { is: "named-deck-button" }); + button.setAttribute("name", name); + button.deckId = deckId; + button.textContent = name.toUpperCase(); + return button; +} + +function makeSection({ name }) { + let view = document.createElement("section"); + view.setAttribute("name", name); + view.textContent = name + name; + return view; +} + +function addSection({ name, deck, buttons }) { + let button = makeButton({ name, deckId: deck.id }); + buttons.appendChild(button); + let view = makeSection({ name }); + deck.appendChild(view); + return { button, view }; +} + +async function runTests({ deck, buttons }) { + const selectedSlot = deck.shadowRoot.querySelector('slot[name="selected"]'); + const getButtonByName = name => buttons.querySelector(`[name="${name}"]`); + + function checkState(name, count, empty = false) { + // Check that the right view is selected. + is(deck.selectedViewName, name, "The right view is selected"); + + // Verify there's one element in the slot. + let slottedEls = selectedSlot.assignedElements(); + if (empty) { + is(slottedEls.length, 0, "The deck is empty"); + } else { + is(slottedEls.length, 1, "There's one visible view"); + is( + slottedEls[0].getAttribute("name"), + name, + "The correct view is in the slot" + ); + } + + // Check that the hidden properties are set. + let sections = deck.querySelectorAll("section"); + is(sections.length, count, "There are the right number of sections"); + for (let section of sections) { + let sectionName = section.getAttribute("name"); + if (sectionName == name) { + is(section.slot, "selected", `${sectionName} is visible`); + } else { + is(section.slot, "", `${sectionName} is hidden`); + } + } + + // Check the right button is selected. + is(buttons.children.length, count, "There are the right number of buttons"); + for (let button of buttons.children) { + let buttonName = button.getAttribute("name"); + let selected = buttonName == name; + is( + button.hasAttribute("selected"), + selected, + `${buttonName} is ${selected ? "selected" : "not selected"}` + ); + } + } + + // Check that the first view is selected by default. + checkState("one", 3); + + // Switch to the third view. + info("Switch to section three"); + getButtonByName("three").click(); + checkState("three", 3); + + // Add a new section, nothing changes. + info("Add section last"); + let last = addSection({ name: "last", deck, buttons }); + checkState("three", 4); + + // We can switch to the new section. + last.button.click(); + info("Switch to section last"); + checkState("last", 4); + + info("Switch view with selectedViewName"); + let shown = BrowserTestUtils.waitForEvent(deck, "view-changed"); + deck.selectedViewName = "two"; + await shown; + checkState("two", 4); + + info("Switch back to the last view to test removing selected view"); + shown = BrowserTestUtils.waitForEvent(deck, "view-changed"); + deck.setAttribute("selected-view", "last"); + await shown; + checkState("last", 4); + + // Removing the selected section leaves the selected slot empty. + info("Remove section last"); + last.button.remove(); + last.view.remove(); + + info("Should not have any selected views"); + checkState("last", 3, true); + + // Setting a missing view will give a "view-changed" event. + info("Set view to a missing name"); + let hidden = BrowserTestUtils.waitForEvent(deck, "view-changed"); + deck.selectedViewName = "missing"; + await hidden; + checkState("missing", 3, true); + + // Adding the view won't trigger "view-changed", but the view will slotted. + info("Add the missing view, it should be shown"); + shown = BrowserTestUtils.waitForEvent(selectedSlot, "slotchange"); + let viewChangedEvent = false; + let viewChangedFn = () => { + viewChangedEvent = true; + }; + deck.addEventListener("view-changed", viewChangedFn); + addSection({ name: "missing", deck, buttons }); + await shown; + deck.removeEventListener("view-changed", viewChangedFn); + ok(!viewChangedEvent, "The view-changed event didn't fire"); + checkState("missing", 4); +} + +async function setup({ beAsync, first, deckId }) { + // Make the deck and buttons. + const deck = document.createElement("named-deck"); + deck.id = deckId; + for (let name of DEFAULT_SECTION_NAMES) { + deck.appendChild(makeSection({ name })); + } + const buttons = document.createElement("button-group"); + for (let name of DEFAULT_SECTION_NAMES) { + buttons.appendChild(makeButton({ name, deckId })); + } + + let ordered; + if (first == "deck") { + ordered = [deck, buttons]; + } else if (first == "buttons") { + ordered = [buttons, deck]; + } else { + throw new Error("Invalid order"); + } + + // Insert them in the specified order, possibly async. + document.body.appendChild(ordered.shift()); + if (beAsync) { + await new Promise(resolve => requestAnimationFrame(resolve)); + } + document.body.appendChild(ordered.shift()); + + return { deck, buttons }; +} + +add_task(async function testNamedDeckAndButtons() { + // Check adding the deck first. + dump("Running deck first tests synchronously"); + await runTests(await setup({ beAsync: false, first: "deck", deckId: "deck-sync" })); + dump("Running deck first tests asynchronously"); + await runTests(await setup({ beAsync: true, first: "deck", deckId: "deck-async" })); + + // Check adding the buttons first. + dump("Running buttons first tests synchronously"); + await runTests(await setup({ beAsync: false, first: "buttons", deckId: "buttons-sync" })); + dump("Running buttons first tests asynchronously"); + await runTests(await setup({ beAsync: true, first: "buttons", deckId: "buttons-async" })); +}); + +add_task(async function testFocusAndClickMixing() { + const waitForAnimationFrame = () => + new Promise(r => requestAnimationFrame(r)); + const sendTab = (e = {}) => { + synthesizeKey("VK_TAB", e); + return waitForAnimationFrame(); + }; + + const firstButton = document.createElement("button"); + document.body.append(firstButton); + + const { deck, buttons: buttonGroup } = await setup({ + beAsync: false, + first: "buttons", + deckId: "focus-click-mixing", + }); + const buttons = buttonGroup.children; + firstButton.focus(); + const secondButton = document.createElement("button"); + document.body.append(secondButton); + + await sendTab(); + is(document.activeElement, buttons[0], "first deck button is focused"); + is(deck.selectedViewName, "one", "first view is shown"); + + await sendTab(); + is(document.activeElement, secondButton, "focus moves out of group"); + + await sendTab({ shiftKey: true }); + is(document.activeElement, buttons[0], "focus moves back to first button"); + + // Click on another tab button, this should make it the focusable button. + synthesizeMouseAtCenter(buttons[1], {}); + await waitForAnimationFrame(); + + is(deck.selectedViewName, "two", "second view is shown"); + + if (document.activeElement != buttons[1]) { + // On Mac the button isn't focused on click, but it is on Windows/Linux. + await sendTab(); + } + is(document.activeElement, buttons[1], "second deck button is focusable"); + + await sendTab(); + is(document.activeElement, secondButton, "focus moved to second plain button"); + + await sendTab({ shiftKey: true }); + is(document.activeElement, buttons[1], "second deck button is focusable"); + + await sendTab({ shiftKey: true }); + is( + document.activeElement, + firstButton, + "next shift-tab moves out of button group" + ); +}); + </script> +</head> +<body> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_navigate_persist.html b/toolkit/content/tests/chrome/test_navigate_persist.html new file mode 100644 index 0000000000..2ef84ddb15 --- /dev/null +++ b/toolkit/content/tests/chrome/test_navigate_persist.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1460639 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1460639</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + function navigateWindowTo(win, url) { + return new Promise(resolve => { + Services.obs.addObserver(function listener(document) { + Services.obs.removeObserver(listener, "document-element-inserted"); + document.addEventListener("DOMContentLoaded", () => { + resolve(); + }, { once: true } ); + }, "document-element-inserted"); + win.location = url; + }); + } + + function promiseMaybeResizeEvent(win, expectedSize) { + return new Promise(resolve => { + // If the size is already as expected, then there may be no resize + // event. + if (win.outerWidth === expectedSize + && win.outerHeight === expectedSize) { + resolve(); + } + win.addEventListener("resize", () => { + resolve(); + }, {once: true}); + }); + } + + function resize(win, size) { + const resizePromise = promiseMaybeResizeEvent(win, size); + win.resizeTo(size, size); + return resizePromise; + } + + async function runTest() { + // Test that persisted window attributes are loaded when a top level + // window is navigated. This mimics the behavior of early first paint by + // first loading about:blank and then navigating to window_navigate_persist.html. + const PERSIST_SIZE = 200; + // First, load the document and resize it so the size is persisted. + let win = window.browsingContext.topChromeWindow + .openDialog("window_navigate_persist.html", "_blank", `chrome,all,dialog=no`); + await SimpleTest.promiseFocus(win); + await resize(win, PERSIST_SIZE); + is(win.outerWidth, PERSIST_SIZE, "Window is resized to desired width"); + is(win.outerHeight, PERSIST_SIZE, "Window is resized to desired height"); + win.close(); + + // Now mimic early first paint. + win = window.browsingContext.topChromeWindow + .openDialog("about:blank", "_blank", `chrome,all,dialog=no`); + await SimpleTest.promiseFocus(win, true); + isnot(win.outerWidth, PERSIST_SIZE, "Initial window width is not the persisted size"); + isnot(win.outerHeight, PERSIST_SIZE, "Initial window height is not the persisted size"); + + await navigateWindowTo(win, "window_navigate_persist.html"); + await promiseMaybeResizeEvent(win, PERSIST_SIZE); + is(win.outerWidth, PERSIST_SIZE, "Window width is persisted"); + is(win.outerHeight, PERSIST_SIZE, "Window height is persisted"); + win.close(); + SimpleTest.finish(); + } + + </script> +</head> +<body onload="runTest()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460639">Mozilla Bug 1460639</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_notificationbox.xhtml b/toolkit/content/tests/chrome/test_notificationbox.xhtml new file mode 100644 index 0000000000..8de985175a --- /dev/null +++ b/toolkit/content/tests/chrome/test_notificationbox.xhtml @@ -0,0 +1,731 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for notificationbox + --> +<window title="Notification Box" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <vbox id="nb"/> + <menupopup id="menupopup" onpopupshown="this.hidePopup()" onpopuphidden="checkPopupClosed()"> + <menuitem label="One"/> + </menupopup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ +const NOTIFICATION_LOCAL_NAME = "notification-message" +SimpleTest.waitForExplicitFinish(); + +var testtag_notificationbox_buttons = [ + { + label: "Button 1", + accesskey: "u", + callback: testtag_notificationbox_buttonpressed, + popup: "menupopup" + } +]; + +var testtag_notificationbox_buttons_nopopup = [ + { + label: "Button 1 No Popup", + accesskey: "u", + callback: testtag_notificationbox_button1pressed, + }, + { + label: "Button 2 No Popup", + accesskey: "u", + callback: testtag_notificationbox_button2pressed, + } +]; + +let testtag_notificationbox_button_l10n = [ + { + "l10n-id": "test-id" + } +]; + +var testtag_notificationbox_links = [ + { + label: "Link 1", + callback: testtag_notificationbox_buttonpressed, + link: "about:mozilla" + }, + { + label: "Button 2", + accesskey: "u", + callback: testtag_notificationbox_buttonpressed, + } +]; + +var testtag_notificationbox_supportpage = [ + { + supportPage: "test1", + }, + { + label: "This is an existing label", + supportPage: "test2", + }, + { + supportPage: "test3", + "l10n-id": "more-specific-id", + }, + { + supportPage: "test4", + label: "legacy label call", + "l10n-id": "modern-fluent-id" + } +]; + +function testtag_notificationbox_buttonpressed(notification, button) +{ + SimpleTest.is(button.localName, "button"); + return false; +} + +let buttonsPressedLog = ""; +function testtag_notificationbox_button1pressed(notification, button) { buttonsPressedLog += "button1"; return true; } +function testtag_notificationbox_button2pressed(notification, button) { buttonsPressedLog += "button2"; return true; } + +function testtag_notificationbox(nb) +{ + testtag_notificationbox_State(nb, "initial", null, 0); + + SimpleTest.is(nb.removeAllNotifications(false), undefined, "initial removeAllNotifications"); + testtag_notificationbox_State(nb, "initial removeAllNotifications", null, 0); + SimpleTest.is(nb.removeAllNotifications(true), undefined, "initial removeAllNotifications immediate"); + testtag_notificationbox_State(nb, "initial removeAllNotifications immediate", null, 0); + + runTimedTests(tests, -1, nb, null); +} + +var notification_last_events = []; +function notification_eventCallback(event) +{ + notification_last_events.push({ actualEvent: event , item: this }); +} + +/** + * For any notifications that have the notification_eventCallback on + * them, we will have recorded instances of those callbacks firing + * and stored them. This checks to see that the expected event types + * are being fired in order, and targeting the right item. + * + * @param {Array<string>} expectedEvents + * The list of event types, in order, that we expect to have been + * fired on the item. + * @param {<xul:notification>} ntf + * The notification we expect the callback to have been fired from. + * @param {string} testName + * The name of the current test, for logging. + */ +function testtag_notification_eventCallback(expectedEvents, ntf, testName) +{ + for (let i = 0; i < expectedEvents; ++i) { + let expected = expectedEvents[i]; + let { actualEvent, item } = notification_last_events[i]; + SimpleTest.is(actualEvent, expected, testName + ": event name"); + SimpleTest.is(item, ntf, testName + ": event item"); + } + notification_last_events = []; +} + +var tests = +[ + { + async test(nb, ntf) { + ntf = await nb.appendNotification("mutable", { + label: "Original", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons); + + ntf.label = "Changed string"; + await ntf.updateComplete; + SimpleTest.is(ntf.messageText.textContent.trim(), "Changed string", "set notification label with string"); + return ntf; + }, + result(nb, ntf) { + nb.removeNotification(ntf); + testtag_notificationbox_State(nb, "set notification label", null, 0); + } + }, + /* + Ensures that buttons created with the "label" parameter have their + label attribute set correctly. + */ + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification"); + const button = ntf.buttonContainer.querySelector("button"); + SimpleTest.is(button.label, "Button 1", "set button label with the 'label' parameter"); + return ntf; + }, + result(nb, ntf) { + nb.removeNotification(ntf); + testtag_notificationbox_State(nb, "set notification label", null, 0); + } + }, + { + /* + Ensures that buttons created with the "l10n-id" parameter have + their "l10n-id" assigned correctly. + */ + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_button_l10n); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification"); + const button = ntf.buttonContainer.querySelector("button"); + SimpleTest.is(button.dataset.l10nId, "test-id", "create notification button with correctly assigned l10n id"); + return ntf; + }, + result(nb, ntf) { + nb.removeNotification(ntf); + testtag_notificationbox_State(nb, "set notification label", null, 0); + } + }, + { + async test(nb, ntf) { + // append a new notification + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + + // check the getNotificationWithValue method + var ntf_found = nb.getNotificationWithValue("note"); + SimpleTest.is(ntf, ntf_found, "getNotificationWithValue note"); + + var none_found = nb.getNotificationWithValue("notenone"); + SimpleTest.is(none_found, null, "getNotificationWithValue null"); + return ntf; + } + }, + { + test(nb, ntf) { + // check that notifications can be removed properly + nb.removeNotification(ntf); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification", null, 0); + } + }, + { + async test(nb, ntf) { + // append a new notification, but now with an event callback + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + eventCallback: notification_eventCallback, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification with callback"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + return ntf; + } + }, + { + test(nb, ntf) { + nb.removeNotification(ntf); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeNotification with callback", + null, 0); + + testtag_notification_eventCallback(["removed"], ntf, "removeNotification()"); + return ntf; + } + }, + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_MEDIUM, + eventCallback: notification_eventCallback, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification with object"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append with callback", ntf, 1); + testtag_notificationbox_State(nb, "append using object", ntf, 1); + testtag_notification_State(nb, ntf, "append object", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_MEDIUM); + return ntf; + } + }, + { + test(rb, ntf) { + // Dismissing the notification instead of removing it should + // fire a dismissed "event" on the callback, followed by + // a removed "event". + ntf.dismiss(); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "called dismiss()", null, 0); + testtag_notification_eventCallback(["dismissed", "removed"], ntf, + "dismiss()"); + return ntf; + } + }, + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_WARNING_LOW, + eventCallback: notification_eventCallback, + }, [{ + label: "Button", + }]); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_WARNING_LOW); + nb.removeNotification(ntf); + + return [1, null]; + } + }, + { + repeat: true, + async test(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + // append a new notification + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification"); + + // Test persistence + ntf.persistence++; + + return [idx, ntf]; + case 2: + case 3: + nb.removeTransientNotifications(); + + return [idx, ntf]; + } + return ntf; + }, + result(nb, arr) { + var idx = arr[0]; + var ntf = arr[1]; + switch (idx) { + case 1: + testtag_notificationbox_State(nb, "notification added", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 1, "persistence is 1"); + + return [++idx, ntf]; + case 2: + testtag_notificationbox_State(nb, "first removeTransientNotifications", ntf, 1); + testtag_notification_State(nb, ntf, "append", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_LOW); + SimpleTest.is(ntf.persistence, 0, "persistence is now 0"); + + return [++idx, ntf]; + case 3: + testtag_notificationbox_State(nb, "second removeTransientNotifications", null, 0); + + this.repeat = false; + } + return ntf; + } + }, + { + async test(nb, ntf) { + // append another notification + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_MEDIUM, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification again"); + return ntf; + }, + result(nb, ntf) { + // check that appending a second notification after removing the first one works + testtag_notificationbox_State(nb, "append again", ntf, 1); + testtag_notification_State(nb, ntf, "append again", "Notification", "note", + "happy.png", nb.PRIORITY_INFO_MEDIUM); + return ntf; + } + }, + { + test(nb, ntf) { + // check the removeCurrentNotification method + nb.removeCurrentNotification(); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "removeCurrentNotification", null, 0); + } + }, + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_HIGH, + }, testtag_notificationbox_buttons); + return ntf; + }, + result(nb, ntf) { + // test the removeAllNotifications method + testtag_notificationbox_State(nb, "append info_high", ntf, 1); + SimpleTest.is(ntf.priority, nb.PRIORITY_INFO_HIGH, + "notification.priority " + nb.PRIORITY_INFO_HIGH); + SimpleTest.is(nb.removeAllNotifications(false), undefined, "removeAllNotifications"); + } + }, + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + eventCallback: notification_eventCallback, + }, testtag_notificationbox_links); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append link notification with callback"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append link with callback", ntf, 1); + + let buttonContainer = ntf.buttonContainer; + let button = buttonContainer.lastElementChild; + SimpleTest.is(button.localName, "button", "button is a button"); + SimpleTest.ok(!button.href, "button href is not set"); + + let link = ntf.querySelector(".notification-link"); + SimpleTest.is(link.localName, "label", "link is a label"); + SimpleTest.is(link.href, "about:mozilla", "link href is correct"); + + SimpleTest.is(nb.removeAllNotifications(false), undefined, "removeAllNotifications"); + } + }, + { + async test(nb, ntf) { + // append a new notification + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons_nopopup); + return ntf; + }, + result(nb, ntf) { + let buttons = nb.currentNotification.buttonContainer.querySelectorAll("* button"); + + buttons[0].focus(); + synthesizeKey(" ", {}); + SimpleTest.is(buttonsPressedLog, "button1", "button 1 with keyboard"); + buttons[1].focus(); + synthesizeKey(" ", {}); + SimpleTest.is(buttonsPressedLog, "button1button2", "button 2 with keyboard"); + + synthesizeMouseAtCenter(buttons[0], {}); + SimpleTest.is(buttonsPressedLog, "button1button2button1", "button 1 with mouse"); + synthesizeMouseAtCenter(buttons[1], {}); + SimpleTest.is(buttonsPressedLog, "button1button2button1button2", "button 2 with mouse"); + + nb.removeAllNotifications(true); + } + }, + { + async test(nb, ntf) { + ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + eventCallback: notification_eventCallback, + }, testtag_notificationbox_supportpage); + await ntf.updateComplete; + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append support page notification"); + return ntf; + }, + result(nb, ntf) { + testtag_notificationbox_State(nb, "append link with callback", ntf, 1); + + let link = ntf.querySelector(".notification-link"); + SimpleTest.is(link.localName, "a", "link 1 is an anchor"); + SimpleTest.is(link.dataset.l10nId, "moz-support-link-text", "link 1 Fluent ID is set"); + SimpleTest.ok(link.href.endsWith("/test1"), "link 1 href is set"); + + link = link.nextElementSibling; + SimpleTest.is(link.localName, "a", "link 2 is an anchor"); + SimpleTest.is(link.dataset.l10nId, "moz-support-link-text", "link 2 Fluent ID is set"); + SimpleTest.ok(!link.value, "label is not assigned to value when using supportPage"); + SimpleTest.ok(link.href.endsWith("/test2"), "link 2 href is set"); + + link = link.nextElementSibling; + SimpleTest.is(link.localName, "a", "link 3 is an anchor"); + SimpleTest.is(link.dataset.l10nId, "more-specific-id", "link 3 Fluent ID is the passed l10n-id"); + SimpleTest.ok(link.href.endsWith("/test3"), "link 3 href is set"); + + link = link.nextElementSibling; + SimpleTest.is(link.localName, "a", "link 4 is an anchor"); + SimpleTest.is(link.dataset.l10nId, "modern-fluent-id", "link 4 Fluent ID is the passed l10n-id"); + SimpleTest.ok(!link.value, "label is not assigned to value when using supportPage"); + SimpleTest.ok(link.href.endsWith("/test4"), "link 4 href is set"); + + SimpleTest.is(nb.removeAllNotifications(false), undefined, "removeAllNotifications"); + } + }, + { + async test(nb, unused) { + // add a number of notifications and check that they are added in order + await nb.appendNotification("4", { label: "Four", priority: nb.PRIORITY_INFO_HIGH }, + testtag_notificationbox_buttons); + await nb.appendNotification("7", { label: "Seven", priority: nb.PRIORITY_WARNING_HIGH }, + testtag_notificationbox_buttons); + await nb.appendNotification("2", { label: "Two", priority: nb.PRIORITY_INFO_LOW }); + await nb.appendNotification("8", { label: "Eight", priority: nb.PRIORITY_CRITICAL_LOW }); + await nb.appendNotification("5", { label: "Five", priority: nb.PRIORITY_WARNING_LOW }); + await nb.appendNotification("6", { label: "Six", priority: nb.PRIORITY_WARNING_HIGH }); + await nb.appendNotification("1", { label: "One", priority: nb.PRIORITY_INFO_LOW }); + await nb.appendNotification("9", { label: "Nine", priority: nb.PRIORITY_CRITICAL_MEDIUM }); + let ntf = await nb.appendNotification("10", { label: "Ten", priority: nb.PRIORITY_CRITICAL_HIGH }); + await nb.appendNotification("3", { label: "Three", priority: nb.PRIORITY_INFO_MEDIUM }); + return ntf; + }, + result(nb, ntf) { + let expectedValue = "3"; + ntf = nb.getNotificationWithValue(expectedValue); + is(nb.currentNotification, ntf, "appendNotification last notification"); + is(nb.currentNotification.getAttribute("value"), expectedValue, "appendNotification order"); + return 1; + } + }, + { + // test closing notifications to make sure that the current notification is still set properly + repeat: true, + test(nb, testidx) { + this.repeat = false; + return undefined; + }, + result(nb, arr) { + let notificationOrder = [4, 7, 2, 8, 5, 6, 1, 9, 10, 3]; + let allNotificationValues = [...nb.stack.children].map(n => n.getAttribute("value")); + is(allNotificationValues.length, notificationOrder.length, "Expected number of notifications"); + for (let i = 0; i < allNotificationValues.length; i++) { + is( + allNotificationValues[i], + notificationOrder[i].toString(), + `Notification ${i} matches` + ); + } + return undefined; + } + }, + { + async test(nb, ntf) { + var exh = false; + try { + await nb.appendNotification("no", { label: "no", priority: -1 }); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too low"); + + exh = false; + try { + await nb.appendNotification("no", { label: "no", priority: 11 }); + } catch (ex) { exh = true; } + SimpleTest.is(exh, true, "appendNotification priority too high"); + + // check that the other priority types work properly + runTimedTests(appendPriorityTests, -1, nb, nb.PRIORITY_WARNING_LOW); + } + } +]; + +var appendPriorityTests = [ + { + async test(nb, priority) { + let ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority, + }, testtag_notificationbox_buttons); + SimpleTest.is(ntf && ntf.localName == NOTIFICATION_LOCAL_NAME, true, "append notification " + priority); + return [ntf, priority]; + }, + result(nb, obj) { + SimpleTest.is(obj[0].priority, obj[1], "notification.priority " + obj[1]); + return obj[1]; + } + }, + { + test(nb, priority) { + nb.removeCurrentNotification(); + return priority; + }, + async result(nb, priority) { + if (priority == nb.PRIORITY_CRITICAL_HIGH) { + let ntf = await nb.appendNotification("note", { + label: "Notification", + image: "happy.png", + priority: nb.PRIORITY_INFO_LOW, + }, testtag_notificationbox_buttons); + setTimeout(checkPopupTest, 50, nb, ntf); + } + else { + runTimedTests(appendPriorityTests, -1, nb, ++priority); + } + } + }, +]; + +function testtag_notificationbox_State(nb, testid, expecteditem, expectedcount) +{ + SimpleTest.is(nb.currentNotification, expecteditem, testid + " currentNotification"); + SimpleTest.is(nb.allNotifications ? nb.allNotifications.length : "no value", + expectedcount, testid + " allNotifications"); +} + +function testtag_notification_State(nb, ntf, testid, label, value, image, priority) +{ + is(ntf.messageText.textContent.trim(), label, testid + " notification label"); + is(ntf.getAttribute("value"), value, testid + " notification value"); + is(ntf.priority, priority, testid + " notification priority"); + + var type; + switch (priority) { + case nb.PRIORITY_INFO_LOW: + case nb.PRIORITY_INFO_MEDIUM: + case nb.PRIORITY_INFO_HIGH: + type = "info"; + break; + case nb.PRIORITY_WARNING_LOW: + case nb.PRIORITY_WARNING_MEDIUM: + case nb.PRIORITY_WARNING_HIGH: + type = "warning"; + break; + case nb.PRIORITY_CRITICAL_LOW: + case nb.PRIORITY_CRITICAL_MEDIUM: + case nb.PRIORITY_CRITICAL_HIGH: + type = "critical"; + break; + } + + is(ntf.getAttribute("type"), type, testid + " notification type"); + + let icons = { + info: "chrome://global/skin/icons/info-filled.svg", + warning: "chrome://global/skin/icons/warning.svg", + critical: "chrome://global/skin/icons/error.svg", + }; + let icon = icons[type]; + is(ntf.messageImage.src, icon, "notification image is set"); +} + +function checkPopupTest(nb, ntf) +{ + if (nb._animating) { + setTimeout(checkPopupTest, 50, nb, ntf); + } else { + var evt = new Event(""); + ntf.dispatchEvent(evt); + evt.target.buttonInfo = testtag_notificationbox_buttons[0]; + ntf.handleEvent(evt); + } +} + +function checkPopupClosed() +{ + SimpleTest.finish(); +} + +/** + * run one or more tests which perform a test operation, wait for a delay, + * then perform a result operation. + * + * tests - array of objects where each object is : + * { + * test: test function, + * result: result function + * repeat: true to repeat the test + * } + * idx - starting index in tests + * element - element to run tests on + * arg - argument to pass between test functions + * + * If, after executing the result part, the repeat property of the test is + * true, then the test is repeated. If the repeat property is not true, + * continue on to the next test. + * + * The test and result functions take two arguments, the element and the arg. + * The test function may return a value which will passed to the result + * function as its arg. The result function may also return a value which + * will be passed to the next repetition or the next test in the array. + */ +async function runTimedTests(tests, idx, element, arg) +{ + if (idx >= 0 && "result" in tests[idx]) + arg = tests[idx].result(element, arg); + + // if not repeating, move on to the next test + if (idx == -1 || !tests[idx].repeat) + idx++; + + if (idx < tests.length) { + let result = await tests[idx].test(element, arg); + setTimeout(runTimedTestsWait, 50, tests, idx, element, result); + } +} + +function runTimedTestsWait(tests, idx, element, arg) +{ + // use this secret property to check if the animation is still running. If it + // is, then the notification hasn't fully opened or closed yet + if (element._animating) + setTimeout(runTimedTestsWait, 50, tests, idx, element, arg); + else + runTimedTests(tests, idx, element, arg); +} + +setTimeout(() => { + testtag_notificationbox(new MozElements.NotificationBox(e => { + document.getElementById("nb").appendChild(e); + })); +}, 0); +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel.xhtml b/toolkit/content/tests/chrome/test_panel.xhtml new file mode 100644 index 0000000000..66cba3f232 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Tests" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_panel.xhtml", "_blank", "chrome,left=200,top=200,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml b/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml new file mode 100644 index 0000000000..4bcf3292e7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_anchoradjust.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Test Panel Position When Anchor Changes" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_panel_anchoradjust.xhtml", "_blank", "chrome,left=200,top=200,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_focus.xhtml b/toolkit/content/tests/chrome/test_panel_focus.xhtml new file mode 100644 index 0000000000..87dc6ec140 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_focus.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +// use a chrome window for this test as the focus in content windows can be +// adjusted by the current selection position + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + // move the mouse so any tooltips that might be open go away, otherwise this + // test can fail on Mac + synthesizeMouse(document.documentElement, 1, 1, { type: "mousemove" }); + + window.openDialog("window_panel_focus.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_panel_hover_menu.xhtml b/toolkit/content/tests/chrome/test_panel_hover_menu.xhtml new file mode 100644 index 0000000000..4c5d8589cb --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_hover_menu.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <meta charset="utf-8" /> + <title><!-- Test with dialog & buttons --></title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <link rel="stylesheet" href="chrome://global/skin"/> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script><![CDATA[ + add_task(async function test_panel_submenu_hover() { + let panel = document.getElementById("panel"); + let menu = document.getElementById("menu"); + let menupopup = document.getElementById("menupopup"); + + let panelShown = new Promise(r => panel.addEventListener("popupshown", r, { once: true })); + info("opening panel"); + panel.openPopupAtScreen(window.screenX, window.screenY); + await panelShown; + info("panel shown"); + + info("hovering menu button"); + synthesizeMouseAtCenter(menu, { type: "mousemove" }); + // Wait for at least the submenu delay. + await new Promise(r => setTimeout(r, 1000)); + is(menupopup.state, "closed", "menu shouldn't have opened"); + + info("clicking menu button"); + let menupopupShown = new Promise(r => menupopup.addEventListener("popupshown", r, { once: true })); + synthesizeMouseAtCenter(menu, {}); + await menupopupShown; + + ok(true, "Menupopup was shown on click"); + }); + ]]></script> +</head> +<body> + <xul:panel id="panel"> + <xul:button type="menu" id="menu" label="Open menu"> + <xul:menupopup id="menupopup"> + <xul:menuitem label="foo"/> + </xul:menupopup> + </xul:button> + </xul:panel> +</body> +</html> diff --git a/toolkit/content/tests/chrome/test_panel_open.xhtml b/toolkit/content/tests/chrome/test_panel_open.xhtml new file mode 100644 index 0000000000..27707a4f34 --- /dev/null +++ b/toolkit/content/tests/chrome/test_panel_open.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + Test for panel 'open' state on the anchor. + --> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<label id="outerlabel" value="Label"/> +<panel id="panel" type="arrow"> + <label id="innerlabel" value="Inner" context="menupopup"/> + <menupopup id="menupopup"> + <menuitem label="One"/> + </menupopup> +</panel> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +add_task(async () => { + // Open a panel and check the open state. The open state should only be assigned + // for arrow panels and not the context menu. + + let panel = document.getElementById("panel"); + let menupopup = document.getElementById("menupopup"); + let innerlabel = document.getElementById("innerlabel"); + let outerlabel = document.getElementById("outerlabel"); + + // Two iterations are used, one with type="arrow" and the second without. + for (let iter = 0; iter < 2; iter++) { + await new Promise(resolve => { + panel.addEventListener("popupshown", resolve, { once: true }); + panel.openPopup(outerlabel, "after_start"); + }); + + // The open state should only be set for arrow panels. + if (panel.getAttribute("type") == "arrow") { + is(outerlabel.getAttribute("open"), "true", "outer label open state when panel opened"); + } else { + ok(!outerlabel.hasAttribute("open"), "outer label open state when panel opened"); + } + ok(!innerlabel.hasAttribute("open"), "inner label open state when panel opened"); + + await new Promise(resolve => { + menupopup.addEventListener("popupshown", resolve, { once: true }); + synthesizeMouse(innerlabel, 4, 4, { type: "contextmenu", button: 2 }); + }); + + // The open state should only be set for arrow panels. + if (panel.getAttribute("type") == "arrow") { + is(outerlabel.getAttribute("open"), "true", "outer label open state when context menu opened"); + } else { + ok(!outerlabel.hasAttribute("open"), "outer label open state when context menu opened"); + } + ok(!innerlabel.hasAttribute("open"), "inner label open state when context menu opened"); + + await new Promise(resolve => { + menupopup.addEventListener("popuphidden", resolve, { once: true }); + menupopup.hidePopup(); + }); + + await new Promise(resolve => { + panel.addEventListener("popuphidden", resolve, { once: true }); + panel.hidePopup(); + }); + + ok(!outerlabel.hasAttribute("open"), "outer label open state when panel closed"); + ok(!innerlabel.hasAttribute("open"), "inner label open state when panel closed"); + + // Clear the type attribute for the second iteration. + panel.removeAttribute("type"); + } +}); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_panelfrommenu.xhtml b/toolkit/content/tests/chrome/test_panelfrommenu.xhtml new file mode 100644 index 0000000000..4a00dc58ab --- /dev/null +++ b/toolkit/content/tests/chrome/test_panelfrommenu.xhtml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Open panel from menuitem" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test does the following: + 1. Opens the menu, causing the popupshown event to fire, which will call menuOpened. + 2. Keyboard events are fired to cause the first item on the menu to be executed. + 3. The command event handler for the first menuitem opens the panel. + 4. As a menuitem was executed, the menu will roll up, hiding it. + 5. The popuphidden event for the menu calls menuClosed which tests the popup states. + 6. The panelOpened function tests the popup states again and hides the popup. + 7. Once the panel's popuphidden event fires, tests are performed to see if + panels inside buttons and toolbarbuttons work. Each is opened and the closed. + --> + +<menu id="menu" onpopupshown="menuOpened()" onpopuphidden="menuClosed();"> + <menupopup> + <menuitem id="i1" label="One" oncommand="$('panel').openPopup($('menu'), 'after_start');"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</menu> + +<panel id="hiddenpanel" hidden="true"/> + +<panel id="panel" onpopupshown="panelOpened()" + onpopuphidden="$('button').focus(); $('button').open = true"> + <html:input/> +</panel> + +<button id="button" type="menu" label="Button"> + <panel onpopupshown="panelOnButtonOpened(this)" + onpopuphidden="$('tbutton').open = true;"> + <button label="OK" oncommand="this.parentNode.parentNode.open = false"/> + </panel> +</button> + +<toolbarbutton id="tbutton" type="menu" label="Toolbarbutton"> + <panel onpopupshown="panelOnToolbarbuttonOpened(this)" + onpopuphidden="SimpleTest.finish()"> + <html:input/> + </panel> +</toolbarbutton> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + is($("hiddenpanel").state, "closed", "hidden popup is closed"); + + var menu = $("menu"); + menu.open = true; +} + +function menuOpened() +{ + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); +} + +function menuClosed() +{ + // the panel will be open at this point, but the popupshown event + // still needs to fire + is($("panel").state, "showing", "panel is open after menu hide"); + is($("menu").menupopup.state, "closed", "menu is closed after menu hide"); +} + +function panelOpened() +{ + is($("panel").state, "open", "panel is open"); + is($("menu").menupopup.state, "closed", "menu is closed"); + $("panel").hidePopup(); +} + +function panelOnButtonOpened(panel) +{ + is(panel.state, 'open', 'button panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from button open"); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, document.documentElement, "focus not modified on cursor down from button"); + panel.firstChild.doCommand() +} + +function panelOnToolbarbuttonOpened(panel) +{ + is(panel.state, 'open', 'toolbarbutton panel is open'); + is(document.activeElement, document.documentElement, "focus blurred on panel from toolbarbutton open"); + panel.firstChild.focus(); + synthesizeKey("KEY_ArrowDown"); + is(document.activeElement, panel.firstChild, "focus not modified on cursor down from toolbarbutton"); + panel.parentNode.open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchor.xhtml b/toolkit/content/tests/chrome/test_popup_anchor.xhtml new file mode 100644 index 0000000000..8825e8fd14 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchor.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_anchor.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml b/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml new file mode 100644 index 0000000000..cc5141fa0b --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_anchoratrect.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_anchoratrect.xhtml", "_blank", "chrome,width=200,height=200,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_attribute.xhtml b/toolkit/content/tests/chrome/test_popup_attribute.xhtml new file mode 100644 index 0000000000..bda23a930c --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_attribute.xhtml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +async function runTest() +{ + // This test exercises non-native menu code. So disable native context menus for this test. + // If we ever get to a point where we don't use any non-native menus on macOS any more, we can + // disable this test on macOS. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + + window.open("window_popup_attribute.xhtml", "_blank", "width=600,height=800"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_button.xhtml b/toolkit/content/tests/chrome/test_popup_button.xhtml new file mode 100644 index 0000000000..d6d77a82da --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_button.xhtml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu Button Popup Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +async function runTest() +{ + // This test exercises non-native menu code. So disable native context menus for this test. + // If we ever get to a point where we don't use any non-native menus on macOS any more, we can + // disable this test on macOS. + await SpecialPowers.pushPrefEnv({ set: [["widget.macos.native-context-menus", false]] }); + + window.open("window_popup_button.xhtml", "_blank", "width=700,height=800"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_coords.xhtml b/toolkit/content/tests/chrome/test_popup_coords.xhtml new file mode 100644 index 0000000000..bc9e042cc5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_coords.xhtml @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Coordinate Tests" + onload="setTimeout(openThePopup, 0, 'outer');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<deck style="margin-top: 5px; padding-top: 5px;"> + <label id="outer" style="display: block" popup="outerpopup" value="Popup"/> +</deck> + +<panel id="outerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); openThePopup('inner')" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); SimpleTest.finish();"> + <button id="item1" label="First"/> + <label id="inner" value="Second" popup="innerpopup"/> + <button id="item2" label="Third"/> +</panel> + +<menupopup id="innerpopup" + onpopupshowing="popupShowingEventOccurred(event);" + onpopupshown="eventOccurred(event); event.target.hidePopup();" + onpopuphiding="eventOccurred(event);" + onpopuphidden="eventOccurred(event); document.getElementById('outerpopup').hidePopup();"> + <menuitem id="inner1" label="Inner First"/> + <menuitem id="inner2" label="Inner Second"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function openThePopup(id) +{ + if (id == "inner") + document.getElementById("item1").focus(); + + var trigger = document.getElementById(id); + synthesizeMouse(trigger, 4, 5, { }); +} + +function eventOccurred(event) +{ + var testname = event.type + " on " + event.target.id + " "; + ok(MouseEvent.isInstance(event), testname + "is a mouse event"); + is(event.clientX, 0, testname + "clientX"); + is(event.clientY, 0, testname + "clientY"); + is(event.rangeParent, null, testname + "rangeParent"); + is(event.rangeOffset, 0, testname + "rangeOffset"); +} + +function popupShowingEventOccurred(event) +{ + // the popupshowing event should have the event coordinates and + // range position filled in. + var testname = "popupshowing on " + event.target.id + " "; + ok(MouseEvent.isInstance(event), testname + "is a mouse event"); + + var trigger = document.getElementById(event.target.id == "outerpopup" ? "outer" : "inner"); + var rect = trigger.getBoundingClientRect(); + is(event.clientX, Math.round(rect.left + 4), testname + "clientX"); + is(event.clientY, Math.round(rect.top + 5), testname + "clientY"); + // rangeOffset should be just at the trigger element, since they are labels + // they don't have any childrens so the offset should be zero. + is(event.rangeParent, trigger, testname + "rangeParent"); + is(event.rangeOffset, 0, testname + "rangeOffset"); + + var popuprect = event.target.getBoundingClientRect(); + var marginLeft = parseFloat(getComputedStyle(event.target).marginLeft); + var marginTop = parseFloat(getComputedStyle(event.target).marginTop); + is(Math.round(popuprect.left - marginLeft), Math.round(rect.left + 4), "popup left"); + is(Math.round(popuprect.top - marginTop), Math.round(rect.top + 5), "popup top"); + ok(popuprect.width > 0, "popup width"); + ok(popuprect.height > 0, "popup height"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_keys.xhtml b/toolkit/content/tests/chrome/test_popup_keys.xhtml new file mode 100644 index 0000000000..6b8dd31143 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_keys.xhtml @@ -0,0 +1,167 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menu ignorekeys Test" + onkeydown="keyDown()" onkeypress="gKeyPressCount++; event.stopPropagation(); event.preventDefault();" + onload="runTests();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks that the ignorekeys attribute can be used on a menu to + disable key navigation. The test is performed twice by opening the menu, + simulating a cursor down key, and closing the popup. When keys are enabled, + the first item on the menu should be highlighted, otherwise the first item + should not be highlighted. + --> + +<menupopup id="popup"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + <menuitem id="i3" label="Three"/> + <menuitem id="i4" label="Four"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +let gIgnoreKeys = false; +let gKeyPressCount = 0; +let gLastFirstMenuActiveValue = null; + + +function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, function eventOccurred(event) { + resolve(); + }, { once: true}); + }); +} + +function runTests() +{ + function promiseFlushingMutationObserver() { + return new Promise(SimpleTest.executeSoon); + } + + (async function() { + const observer = new MutationObserver(checkIfFirstMenuItemActive); + observer.observe($("i1"), { attributes: true }); + + var popup = $("popup"); + is(popup.hasAttribute("ignorekeys"), false, "keys enabled"); + + let popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + let popupHiddenPromise = waitForEvent(popup, "popuphidden"); + info("Synthesizing ArrowDown (no ignorekeys)..."); + synthesizeKey("KEY_ArrowDown"); + await popupHiddenPromise; + + is(gKeyPressCount, 0, "keypresses with ignorekeys='false'"); + + gIgnoreKeys = true; + popup.setAttribute("ignorekeys", "true"); + // clear this first to avoid confusion + observer.disconnect(); + $("i1").removeAttribute("_moz-menuactive") + await promiseFlushingMutationObserver(); + observer.observe($("i1"), { attributes: true }); + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + info("Synthesizing ArrowDown (ignorekeys=\"true\")..."); + synthesizeKey("KEY_ArrowDown"); + + await new Promise(resolve => setTimeout(() => resolve(), 1000)); + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + await popupHiddenPromise; + + is(gKeyPressCount, 1, "keypresses with ignorekeys='true'"); + + popup.setAttribute("ignorekeys", "shortcuts"); + // clear this first to avoid confusion + observer.disconnect(); + $("i1").removeAttribute("_moz-menuactive") + await promiseFlushingMutationObserver(); + observer.observe($("i1"), { attributes: true }); + + popupShownPromise = waitForEvent(popup, "popupshown"); + popup.openPopup(null, "after_start"); + await popupShownPromise; + + // When ignorekeys="shortcuts", T should be handled but accel+T should propagate. + info("Synthesizing \"t\"..."); + sendString("t"); + is(gKeyPressCount, 1, "keypresses after t pressed with ignorekeys='shortcuts'"); + + info("Synthesizing Accel-T..."); + synthesizeKey("t", { accelKey: true }); + is(gKeyPressCount, 2, "keypresses after accel+t pressed with ignorekeys='shortcuts'"); + + popupHiddenPromise = waitForEvent(popup, "popuphidden"); + popup.hidePopup(); + await popupHiddenPromise; + + observer.disconnect(); + SimpleTest.finish(); + })(); +} + +function checkIfFirstMenuItemActive(aMutationList) { + for (const mutation of aMutationList) { + if (mutation.type != "attributes" || mutation.attributeName != "_moz-menuactive") { + continue; + } + + // the attribute should not be changed when ignorekeys is enabled + if (gIgnoreKeys) { + ok(false, "move key with keys disabled"); + return; + } + + is( + $("popup").hasAttribute("ignorekeys") + ? gLastFirstMenuActiveValue + : $("i1").getAttribute("_moz-menuactive"), + "true", + "move key with keys enabled" + ); + $("popup").hidePopup(); + gLastFirstMenuActiveValue = null; + break; + } +} + +function keyDown() { + // when keys are enabled, the menu should have stopped propagation of the + // event, so a bubbling listener for a keydown event should only occur + // when keys are disabled. + ok(gIgnoreKeys, "key listener fired with keys " + + (gIgnoreKeys ? "disabled" : "enabled")); + gLastFirstMenuActiveValue = $("i1").getAttribute("_moz-menuactive"); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml new file mode 100644 index 0000000000..a7800caaad --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_moveToAnchor.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<vbox align="start" style="margin-left: 80px;"> + <button id="button1" label="Button 1" style="margin-top: 80px;"/> + <button id="button2" label="Button 2" style="margin-top: 70px;"/> +</vbox> + +<menupopup id="popup" onpopupshown="popupshown()" onpopuphidden="SimpleTest.finish()"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<script> +SimpleTest.waitForExplicitFinish(); + +function runTest(id) +{ + $("popup").openPopup($("button1"), "after_start"); +} + +function popupshown() +{ + var popup = $("popup"); + var popupheight = popup.getBoundingClientRect().height; + var button1rect = $("button1").getBoundingClientRect(); + var button2rect = $("button2").getBoundingClientRect(); + var marginLeft = parseFloat(getComputedStyle(popup).marginLeft); + var marginTop = parseFloat(getComputedStyle(popup).marginTop); + + checkCoords(popup, button1rect.left + marginLeft, button1rect.bottom + marginTop, "initial"); + + popup.moveToAnchor($("button1"), "after_start", 0, 8); + checkCoords(popup, button1rect.left + marginLeft, button1rect.bottom + 8 + marginTop, "move anchor top + 8"); + + popup.moveToAnchor($("button1"), "after_start", 6, -10); + checkCoords(popup, button1rect.left + 6 + marginLeft, button1rect.bottom - 10 + marginTop, "move anchor left + 6, top - 10"); + + popup.moveToAnchor($("button1"), "before_start", -2, 0); + checkCoords(popup, button1rect.left - 2 + marginLeft, button1rect.top - popupheight - marginTop, "move anchor before_start"); + + popup.moveToAnchor($("button2"), "before_start"); + checkCoords(popup, button2rect.left + marginLeft, button2rect.top - popupheight - marginTop, "move button2"); + + popup.moveToAnchor($("button1"), "end_before"); + checkCoords(popup, button1rect.right + marginLeft, button1rect.top + marginTop, "move anchor end_before"); + + popup.moveToAnchor($("button2"), "after_start", 5, 4); + checkCoords(popup, button2rect.left + 5 + marginLeft, button2rect.bottom + 4 + marginTop, "move button2 left + 5, top + 4"); + + popup.moveTo($("button1").screenX + 10, $("button1").screenY + 12); + checkCoords(popup, button1rect.left + 10, button1rect.top + 12, "move to button1 screen with offset"); + + popup.moveToAnchor($("button1"), "after_start", 1, 2); + checkCoords(popup, button1rect.left + 1 + marginLeft, button1rect.bottom + 2 + marginTop, "move button2 after screen"); + + popup.hidePopup(); +} + +function checkCoords(popup, expectedx, expectedy, testid) +{ + var rect = popup.getBoundingClientRect(); + is(Math.round(rect.left), Math.round(expectedx), testid + " left"); + is(Math.round(rect.top), Math.round(expectedy), testid + " top"); +} + +SimpleTest.waitForFocus(runTest); + +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml b/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml new file mode 100644 index 0000000000..916fb7eb86 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Prevent Default Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event and that preventDefault has no effect for the popuphiding event. + --> + +<script> +SimpleTest.waitForExplicitFinish(); + +var gBlockShowing = true; +var gShownNotAllowed = true; + +function runTest() +{ + document.getElementById("menu").open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + // since this is a content test, preventDefault should have no effect + event.preventDefault(); +} + +function popupHidden() +{ + ok(true, "popuphiding preventDefault not allowed"); + SimpleTest.finish(); +} +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml new file mode 100644 index 0000000000..d3e62d05c7 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_preventdefault_chrome.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Attribute Tests" + onload="setTimeout(runTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_popup_preventdefault_chrome.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_recreate.xhtml b/toolkit/content/tests/chrome/test_popup_recreate.xhtml new file mode 100644 index 0000000000..b1922ea51b --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_recreate.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Recreate Test" + onload="setTimeout(init, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This is a test for bug 388361. + + This test checks that a menulist's popup is properly created and sized when + the popup node is removed and another added in its place. + + --> + +<script> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +var gState = "before"; + +function init() +{ + document.getElementById("menulist").open = true; +} + +function isWithinHalfPixel(a, b) +{ + return Math.abs(a - b) <= 0.5; +} + +const ismac = navigator.platform.indexOf("Mac") == 0; +function inputMargin(el) { + let cs = getComputedStyle(el); + // XXX Internal properties are not exposed in getComputedStyle, so we have to + // use margin and rely on our knowledge of them matching negative margins + // where appropriate. + // return parseFloat(cs.getPropertyValue("-moz-window-input-region-margin")); + return ismac ? 0 : Math.max(-parseFloat(cs.marginLeft), 0); +} + +function recreate() +{ + if (gState == "before") { + var element = document.getElementById("menulist"); + while (element.hasChildNodes()) + element.firstChild.remove(); + element.appendItem("Cat"); + gState = "after"; + document.getElementById("menulist").open = true; + } + else { + SimpleTest.finish(); + } +} + +function checkSize() +{ + var menulist = document.getElementById("menulist"); + var menurect = menulist.getBoundingClientRect(); + var popuprect = menulist.menupopup.getBoundingClientRect(); + + let marginLeft = parseFloat(getComputedStyle(menulist.menupopup).marginLeft); + ok(isWithinHalfPixel(menurect.left + marginLeft, popuprect.left), "left position " + gState); + ok(isWithinHalfPixel(menurect.right + marginLeft + 2 * inputMargin(menulist.menupopup), popuprect.right), "right position " + gState); + ok(Math.round(popuprect.right) - Math.round(popuprect.left) > 0, "height " + gState) + document.getElementById("menulist").open = false; +} +]]> +</script> + +<hbox align="center" pack="center"> + <menulist id="menulist" onpopupshown="checkSize();" onpopuphidden="recreate();" style="width: 200px"> + <menupopup position="after_start"> + <menuitem label="Cat"/> + </menupopup> + </menulist> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_scaled.xhtml b/toolkit/content/tests/chrome/test_popup_scaled.xhtml new file mode 100644 index 0000000000..eda4d7231e --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_scaled.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popups in Scaled Content" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- This test checks that the position is correct in two cases: + - a popup anchored at an element in a scaled document + - a popup opened at a screen coordinate in a scaled window + --> + +<iframe id="frame" width="60" height="140" + src="data:text/html,<html><body><input size='4' id='one'><input size='4' id='two'></body></html>"/> + +<menupopup id="popup" onpopupshown="shown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> +</menupopup> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var screenTest = false; +var screenx = -1, screeny = -1; + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + setScale($("frame").contentWindow, 2); + + var anchor = $("frame").contentDocument.getElementById("two"); + anchor.getBoundingClientRect(); // flush to update display after scale change + $("popup").openPopup(anchor, "after_start"); +} + +function setScale(win, scale) +{ + SpecialPowers.setFullZoom(win, scale); +} + +function shown() +{ + var popup = $("popup"); + var marginLeft = parseFloat(getComputedStyle(popup).marginLeft); + var marginTop = parseFloat(getComputedStyle(popup).marginTop); + if (screenTest) { + is(popup.screenX - marginLeft, screenx, "screen left position"); + is(popup.screenY - marginTop, screeny, "screen top position"); + } else { + var anchor = $("frame").contentDocument.getElementById("two"); + is(Math.round(anchor.getBoundingClientRect().left * 2), + Math.round(popup.getBoundingClientRect().left - marginLeft), "anchored left position"); + is(Math.round(anchor.getBoundingClientRect().bottom * 2), + Math.round(popup.getBoundingClientRect().top - marginTop), "anchored top position"); + } + + popup.hidePopup(); +} + +function nextTest() +{ + if (screenTest) { + setScale(window, 1); + SimpleTest.finish(); + } + else { + screenTest = true; + var rootElement = document.documentElement; + + setScale(window, 2); + // - the iframe will be at 4×, but out here css pixels are only 2× device pixels + + requestAnimationFrame(() => requestAnimationFrame(() => { + screenx = rootElement.screenX + 20; + screeny = rootElement.screenY + 20; + $("popup").openPopupAtScreen(screenx, screeny); + })); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popup_tree.xhtml b/toolkit/content/tests/chrome/test_popup_tree.xhtml new file mode 100644 index 0000000000..23a37e339d --- /dev/null +++ b/toolkit/content/tests/chrome/test_popup_tree.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tree in Popup Test" + onload="setTimeout(runTests, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<panel id="panel" onpopupshown="treeClick()" onpopuphidden="SimpleTest.finish()"> + <tree id="tree" width="350" rows="5"> + <treecols> + <treecol id="name" label="Name" flex="1"/> + <treecol id="address" label="Street" flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem> + <treerow> + <treecell label="Justin Thyme"/> + <treecell label="800 Bay Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary Goround"/> + <treecell label="47 University Avenue"/> + </treerow> + </treeitem> + </treechildren> + </tree> +</panel> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests() +{ + $("panel").openPopup(null, "overlap", 2, 2); +} + +function treeClick() +{ + var tree = $("tree"); + is(tree.currentIndex, -1, "selectedIndex before click"); + synthesizeMouseExpectEvent($("treechildren"), 2, 2, { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 0, "selectedIndex after click"); + + var rect = tree.getCoordsForCellItem(1, tree.columns.address, ""); + synthesizeMouseExpectEvent($("treechildren"), rect.x, rect.y + 2, + { }, $("treechildren"), "click", ""); + is(tree.currentIndex, 1, "selectedIndex after second click " + rect.x + "," + rect.y); + + $("panel").hidePopup(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popuphidden.xhtml b/toolkit/content/tests/chrome/test_popuphidden.xhtml new file mode 100644 index 0000000000..aa4b5aff1e --- /dev/null +++ b/toolkit/content/tests/chrome/test_popuphidden.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Hidden Popup Test" + onload="setTimeout(runTests, 0, $('popup'));" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<menupopup id="popup" hidden="true" onpopupshown="ok(true, 'popupshown'); this.hidePopup()" + onpopuphidden="$('popup-hideonshow').openPopup(null, 'after_start')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<menupopup id="popup-hideonshow" onpopupshowing="hidePopupWhileShowing(this)" + onpopupshown="ok(false, 'popupshown when hidden')"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> +</menupopup> + +<button id="button" type="menu" label="Menu"> + <menupopup id="popupinbutton" hidden="true" + onpopupshown="ok(true, 'popupshown'); ok($('button').open, 'open'); this.hidden = true;"> + <menuitem id="i1" label="One"/> + <menuitem id="i2" label="Two"/> + </menupopup> +</button> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTests(popup) +{ + const observer = new MutationObserver(checkEndTest); + observer.observe($("button"), { attributes: true }); + popup.hidden = false; + popup.openPopup(null, "after_start"); +} + +function hidePopupWhileShowing(popup) +{ + popup.hidden = true; + popup.clientWidth; // flush layout + is(popup.state, 'closed', 'popupshowing hidden'); + SimpleTest.executeSoon(() => runTests($('popupinbutton'))); +} + +let finished = false; +function checkEndTest(aMutationList, aObserver) +{ + if (finished) { + return; // XXX I don't know why this is necessary. + } + const button = $("button"); + for (const mutation of aMutationList) { + if (mutation.attributeName != "open" || button.hasAttribute("open")) { + continue; + } + + ok($("popupinbutton").hidden, "popup hidden"); + is($("popupinbutton").state, "closed", "popup state"); + ok(!button.open, "not open after hidden"); + aObserver.disconnect(); + SimpleTest.finish(); + finished = true; + return; + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupincontent.xhtml b/toolkit/content/tests/chrome/test_popupincontent.xhtml new file mode 100644 index 0000000000..9721566372 --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupincontent.xhtml @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup in Content Positioning Tests" + onload="setTimeout(nextTest, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks that popups in content areas don't extend past the content area. + --> + +<hbox> + <spacer width="100"/> + <menu id="menu" label="Menu"> + <menupopup style="margin:10px;-moz-window-input-region-margin:0;" id="popup" onpopupshown="popupShown()" onpopuphidden="nextTest()"> + <menuitem label="One"/> + <menuitem label="Two"/> + <menuitem label="Three"/> + <menuitem label="A final longer label that is actually quite long. Very long indeed."/> + </menupopup> + </menu> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var step = ""; +var originalHeight = -1; + +function nextTest() +{ + // there are five tests here: + // openPopupAtScreen - checks that opening a popup using openPopupAtScreen + // constrains the popup to the content area + // left and top - check with the left and top attributes set + // open near bottom - open the menu near the bottom of the window + // large menu - try with a menu that is very large and should be scaled + // shorter menu again - try with a menu that is shorter again. It should have + // the same height as the 'left and top' test + var popup = $("popup"); + var menu = $("menu"); + switch (step) { + case "": + step = "openPopupAtScreen"; + popup.openPopupAtScreen(1000, 1200); + break; + case "openPopupAtScreen": + step = "left and top"; + popup.setAttribute("left", "800"); + popup.setAttribute("top", "2900"); + synthesizeMouse(menu, 2, 2, { }); + break; + case "left and top": + step = "open near bottom"; + // request that the menu be opened with a target point near the bottom of the window, + // so that the menu's top margin will push it completely outside the window. + popup.setAttribute("top", document.documentElement.screenY + window.innerHeight - 5); + synthesizeMouse(menu, 2, 2, { }); + break; + case "open near bottom": + step = "large menu"; + popup.removeAttribute("left"); + popup.removeAttribute("top"); + for (let i = 0; i < 80; i++) + menu.appendItem("Test", ""); + synthesizeMouse(menu, 2, 2, { }); + break; + case "large menu": + step = "shorter menu again"; + for (let i = 0; i < 80; i++) + popup.lastChild.remove(); + synthesizeMouse(menu, 2, 2, { }); + break; + case "shorter menu again": + SimpleTest.finish(); + break; + } +} + +async function popupShown() +{ + // Popup may have wrong initial size in non e10s mode tests, because + // layout is not yet ready for popup content lazy population on + // popupshowing event. + await new Promise(r => + requestAnimationFrame(() => requestAnimationFrame(r)) + ); + + var windowrect = document.documentElement.getBoundingClientRect(); + var popuprect = $("popup").getBoundingClientRect(); + + // subtract one off the edge due to a rounding issue + ok(popuprect.left >= windowrect.left, step + " left"); + ok(popuprect.right - 1 <= windowrect.right, step + " right"); + + if (step == "left and top") { + originalHeight = popuprect.bottom - popuprect.top; + } + else if (step == "open near bottom") { + // check that the menu flipped up so it's above our requested point + ok(popuprect.bottom - 1 <= windowrect.bottom - 5, step + " bottom"); + } + else if (step == "large menu") { + // add 10 to account for the margin + is(popuprect.top, $("menu").getBoundingClientRect().bottom + 10, step + " top"); + ok(popuprect.bottom == windowrect.bottom || + popuprect.bottom - 1 == windowrect.bottom, step + " bottom"); + } + else { + ok(popuprect.top >= windowrect.top, step + " top"); + ok(popuprect.bottom - 1 <= windowrect.bottom, step + " bottom"); + if (step == "shorter menu again") + is(popuprect.bottom - popuprect.top, originalHeight, step + " height shortened"); + } + + $("menu").open = false; +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_popupremoving.xhtml b/toolkit/content/tests/chrome/test_popupremoving.xhtml new file mode 100644 index 0000000000..babd62c18b --- /dev/null +++ b/toolkit/content/tests/chrome/test_popupremoving.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Removing Tests" + onload="setTimeout(nextTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This test checks that popup elements can be removed in various ways without + crashing. It tests two situations, one with menus that are 'separate', and + one with menus that are 'nested'. In each case, there are four levels of menu. + + The nextTest function starts the process by opening the first menu. A set of + popupshown event listeners are used to open the next menu until all four are + showing. This last one calls removePopup to remove the menu node from the + tree. This should hide the popups as they are no longer in a document. + + A mutation listener is triggered when the fourth menu closes by having its + open attribute cleared. This listener hides the third popup which causes + its frame to be removed. Naturally, we want to ensure that this doesn't + crash when the third menu is removed. + --> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox> + +<menu id="nestedmenu1" label="1"> + <menupopup id="nestedpopup1" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu2" label="2"> + <menupopup id="nestedpopup2" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu3" label="3"> + <menupopup id="nestedpopup3" onpopupshown="if (event.target == this) this.firstChild.open = true"> + <menu id="nestedmenu4" label="4"> + <menupopup id="nestedpopup4"> + <menuitem label="Nested 1"/> + <menuitem label="Nested 2"/> + <menuitem label="Nested 3"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> +</menu> + +<menu id="separatemenu1" label="1"> + <menupopup id="separatepopup1" onpopupshown="$('separatemenu2').open = true"> + <menuitem label="L1 One"/> + <menuitem label="L1 Two"/> + <menuitem label="L1 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu2" label="2"> + <menupopup id="separatepopup2" onpopupshown="$('separatemenu3').open = true" + onpopuphidden="popup2Hidden()"> + <menuitem label="L2 One"/> + <menuitem label="L2 Two"/> + <menuitem label="L2 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu3" label="3" onpopupshown="$('separatemenu4').open = true"> + <menupopup id="separatepopup3"> + <menuitem label="L3 One"/> + <menuitem label="L3 Two"/> + <menuitem label="L3 Three"/> + </menupopup> +</menu> + +<menu id="separatemenu4" label="4" onpopuphidden="$('separatemenu2').open = false"> + <menupopup id="separatepopup3"> + <menuitem label="L4 One"/> + <menuitem label="L4 Two"/> + <menuitem label="L4 Three"/> + </menupopup> +</menu> + +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +let gKey = ""; +let gTriggerMutation = null; +let gChangeMutation = null; +let gResolveRemovePopups = null; +let gResolvePopup2Hidden = null; + +function nextTest() +{ + let promiseRemovePopups, promisePopup2Hidden; + if (gKey == "") { + gKey = "separate"; + $("separatemenu4").addEventListener("popupshown", removePopups); + promiseRemovePopups = new Promise(resolve => gResolveRemovePopups = resolve); + promisePopup2Hidden = new Promise(resolve => gResolvePopup2Hidden = resolve); + } + else if (gKey == "separate") { + gKey = "nested"; + $("nestedmenu4").addEventListener("popupshown", removePopups); + promiseRemovePopups = new Promise(resolve => gResolveRemovePopups = resolve); + gResolvePopup2Hidden = null; + } + else { + SimpleTest.finish(); + return; + } + + SimpleTest.executeSoon(async () => { + $(gKey + "menu1").open = true; + await Promise.all([promiseRemovePopups, promisePopup2Hidden]); + nextTest(); + }); +} + +function modified(aMutationList, aObserver) { + // use this mutation listener to hide the third popup, destroying its frame. + // It gets triggered when the open attribute is cleared on the fourth menu. + for (const mutation of aMutationList) { + if (mutation.attributeName != "open") { + continue; + } + gChangeMutation.hidden = true; + // force a layout flush + document.documentElement.clientWidth; + gChangeMutation = null; + aObserver.disconnect(); + break; + } +} + +async function removePopups() +{ + var menu2 = $(gKey + "menu2"); + var menu3 = $(gKey + "menu3"); + is(menu2.getAttribute("open"), "true", gKey + " menu 2 open before"); + is(menu3.getAttribute("open"), "true", gKey + " menu 3 open before"); + + const observer = new MutationObserver(modified); + observer.observe(menu3, { attributes: true }); + gChangeMutation = $(gKey + "menu4"); + var menu = $(gKey + "menu1"); + menu.remove(); + const key = gKey; + await new Promise (SimpleTest.executeSoon); + if (key == "nested") { + // the 'separate' test checks this during the popup2 hidden event handler + is(menu2.hasAttribute("open"), false, gKey + " menu 2 open after"); + is(menu3.hasAttribute("open"), false, gKey + " menu 3 open after"); + } + gResolveRemovePopups(); +} + +function popup2Hidden() { + is($(gKey + "menu2").hasAttribute("open"), false, gKey + " menu 2 open after"); + if (gResolvePopup2Hidden) { + gResolvePopup2Hidden(); + } +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_position.xhtml b/toolkit/content/tests/chrome/test_position.xhtml new file mode 100644 index 0000000000..8c9a0e1b61 --- /dev/null +++ b/toolkit/content/tests/chrome/test_position.xhtml @@ -0,0 +1,130 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for positioning + --> +<window title="position" width="500" height="600" + onload="setTimeout(runTest, 0);" + style="margin: 0; border: 0; padding; 0;" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + +<hbox id="box1"> + <button label="0" style="width: 100px; height: 40px; margin: 3px;"/> +</hbox> +<scrollbox id="box2" orient="vertical" align="start" + style="height: 50px; overflow: hidden; margin-left: 2px; padding: 1px;"> + <deck> + <scrollbox id="box3" orient="vertical" align="start" + style="height: 100px; overflow: scroll; margin: 1px; padding: 0;"> + <vbox id="innerscroll" style="width: 200px" align="start"> + <button id="button1" label="1" style="width: 90px; max-width: 100px; min-width: 80px; min-height: 25px; height: 35px; max-height: 50px; margin: 5px; border: 4px; padding: 7px; appearance: none;"/> + <menu id="menu"> + <menupopup id="popup" style="appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="menuOpened()" + onpopuphidden="if (event.target == this) SimpleTest.finish()"> + <menuitem label="One"/> + <menu id="submenu" label="Three"> + <menupopup id="subpopup" style="appearance: none; margin:0; border: 0; padding: 0;" + onpopupshown="submenuOpened()"> + <menuitem label="Four"/> + </menupopup> + </menu> + </menupopup> + </menu> + <button label="2" style="max-width: 100px; max-height: 20px; margin: 5px;"/> + <button label="3" style="max-width: 100px; max-height: 20px; margin: 5px;"/> + <button label="4" style="max-width: 100px; max-height: 20px; margin: 5px;"/> + </vbox> + <box style="height: 200px"/> + </scrollbox> + </deck> +</scrollbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var winwidth = document.documentElement.getBoundingClientRect().width; + + var box1 = $("box1"); + checkPosition("box1", box1, 0, 0, winwidth, 46); + + var box2 = $("box2"); + checkPosition("box2", box2, 2, 46, winwidth, 96); + + // height is height(box1) = 46 + margin-top(box3) = 1 + margin-top(button1) = 5 + var button1 = $("button1"); + checkPosition("button1", button1, 9, 53, 99, 88); + + box2.scrollTo(7, 16); + + // clientRect height is offset from root so is 16 pixels vertically less + checkPosition("button1 scrolled", button1, 9, 37, 99, 72); + + var box3 = $("box3"); + box3.scrollTo(1, 2); + + checkPosition("button1 scrolled", button1, 9, 35, 99, 70); + + $("menu").open = true; +} + +function menuOpened() +{ + $("submenu").open = true; +} + +function submenuOpened() +{ + var menu = $("menu"); + var menuleft = Math.round(menu.getBoundingClientRect().left); + var menubottom = Math.round(menu.getBoundingClientRect().bottom); + + var submenu = $("submenu"); + var submenutop = Math.round(submenu.getBoundingClientRect().top); + var submenuright = Math.round(submenu.getBoundingClientRect().right); + + checkPosition("popup", $("popup"), menuleft, menubottom, -1, -1); + checkPosition("subpopup", $("subpopup"), submenuright, submenutop, -1, -1); + + menu.open = false; +} + +function checkPosition(testid, elem, cleft, ctop, cright, cbottom) +{ + // -1 for right or bottom means that the exact size should not be + // checked, just ensure it is larger then the left or top position + var rect = elem.getBoundingClientRect(); + is(Math.round(rect.left), cleft, testid + " client rect left"); + if (testid != "popup") + is(Math.round(rect.top), ctop, testid + " client rect top"); + if (cright >= 0) + is(Math.round(rect.right), cright, testid + " client rect right"); + else + ok(rect.right - rect.left > 20, testid + " client rect right"); + if (cbottom >= 0) + is(Math.round(rect.bottom), cbottom, testid + " client rect bottom"); + else + ok(rect.bottom - rect.top > 15, testid + " client rect bottom"); +} + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences.xhtml b/toolkit/content/tests/chrome/test_preferences.xhtml new file mode 100644 index 0000000000..0b3ddf88be --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences.xhtml @@ -0,0 +1,525 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="RunTest();"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + + const kPref = SpecialPowers.Services.prefs; + + // preference values, set 1 + const kPrefValueSet1 = { + int: 23, + bool: true, + string: "rheeet!", + unichar: "äöüßÄÖÜ", + wstring_data: "日本語", + file_data: "/", + + wstring: Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ), + file: Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile), + }; + kPrefValueSet1.wstring.data = kPrefValueSet1.wstring_data; + SafeFileInit(kPrefValueSet1.file, kPrefValueSet1.file_data); + + // preference values, set 2 + const kPrefValueSet2 = { + int: 42, + bool: false, + string: "Mozilla", + unichar: "áôùšŽ", + wstring_data: "헤드라인A", + file_data: "/home", + + wstring: Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ), + file: Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile), + }; + kPrefValueSet2.wstring.data = kPrefValueSet2.wstring_data; + SafeFileInit(kPrefValueSet2.file, kPrefValueSet2.file_data); + + function SafeFileInit(aFile, aPath) { + // set file path without dying for exceptions + try { + aFile.initWithPath(aPath); + } catch {} + } + + function CreateEmptyPrefValueSet() { + var result = { + int: undefined, + bool: undefined, + string: undefined, + unichar: undefined, + wstring_data: undefined, + file_data: undefined, + wstring: undefined, + file: undefined, + }; + return result; + } + + function WritePrefsToSystem(aPrefValueSet) { + // write preference data via XPCOM + kPref.setIntPref("tests.static_preference_int", aPrefValueSet.int); + kPref.setBoolPref("tests.static_preference_bool", aPrefValueSet.bool); + kPref.setCharPref("tests.static_preference_string", aPrefValueSet.string); + kPref.setStringPref("tests.static_preference_unichar", aPrefValueSet.unichar); + kPref.setComplexValue( + "tests.static_preference_wstring", + Ci.nsIPrefLocalizedString, + aPrefValueSet.wstring + ); + kPref.setComplexValue( + "tests.static_preference_file", + Ci.nsIFile, + aPrefValueSet.file + ); + } + + function ReadPrefsFromSystem() { + // read preference data via XPCOM + var result = CreateEmptyPrefValueSet(); + // eslint-disable-next-line mozilla/use-default-preference-values + try { + result.int = kPref.getIntPref("tests.static_preference_int"); + } catch {} + // eslint-disable-next-line mozilla/use-default-preference-values + try { + result.bool = kPref.getBoolPref("tests.static_preference_bool"); + } catch {} + // eslint-disable-next-line mozilla/use-default-preference-values + try { + result.string = kPref.getCharPref("tests.static_preference_string"); + } catch {} + try { + result.unichar = kPref.getStringPref("tests.static_preference_unichar"); + } catch {} + try { + result.wstring = kPref.getComplexValue( + "tests.static_preference_wstring", + Ci.nsIPrefLocalizedString + ); + result.wstring_data = result.wstring.data; + } catch {} + try { + result.file = kPref.getComplexValue( + "tests.static_preference_file", + Ci.nsIFile + ); + result.file_data = result.file.data; + } catch {} + return result; + } + + function GetXULElement(aPrefWindow, aID) { + return aPrefWindow.document.getElementById(aID); + } + + function GetPreference(aPrefWindow, aID) { + return aPrefWindow.Preferences.get(aID); + } + + function WritePrefsToPreferences(aPrefWindow, aPrefValueSet) { + // write preference data into Preference instances + GetPreference(aPrefWindow, "tests.static_preference_int").value = + aPrefValueSet.int; + GetPreference(aPrefWindow, "tests.static_preference_bool").value = + aPrefValueSet.bool; + GetPreference(aPrefWindow, "tests.static_preference_string").value = + aPrefValueSet.string; + GetPreference(aPrefWindow, "tests.static_preference_unichar").value = + aPrefValueSet.unichar; + GetPreference(aPrefWindow, "tests.static_preference_wstring").value = + aPrefValueSet.wstring_data; + GetPreference(aPrefWindow, "tests.static_preference_file").value = + aPrefValueSet.file_data; + } + + function ReadPrefsFromPreferences(aPrefWindow) { + // read preference data from Preference instances + var result = { + int: GetPreference(aPrefWindow, "tests.static_preference_int").value, + bool: GetPreference(aPrefWindow, "tests.static_preference_bool").value, + string: GetPreference(aPrefWindow, "tests.static_preference_string").value, + unichar: GetPreference(aPrefWindow, "tests.static_preference_unichar") + .value, + wstring_data: GetPreference(aPrefWindow, "tests.static_preference_wstring") + .value, + file_data: GetPreference(aPrefWindow, "tests.static_preference_file").value, + wstring: Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ), + file: Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile), + }; + result.wstring.data = result.wstring_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + function WritePrefsToUI(aPrefWindow, aPrefValueSet) { + // write preference data into UI elements + GetXULElement(aPrefWindow, "static_element_int").value = aPrefValueSet.int; + GetXULElement(aPrefWindow, "static_element_bool").checked = + aPrefValueSet.bool; + GetXULElement(aPrefWindow, "static_element_string").value = + aPrefValueSet.string; + GetXULElement(aPrefWindow, "static_element_unichar").value = + aPrefValueSet.unichar; + GetXULElement(aPrefWindow, "static_element_wstring").value = + aPrefValueSet.wstring_data; + GetXULElement(aPrefWindow, "static_element_file").value = + aPrefValueSet.file_data; + } + + function ReadPrefsFromUI(aPrefWindow) { + // read preference data from Preference instances + var result = { + int: GetXULElement(aPrefWindow, "static_element_int").value, + bool: GetXULElement(aPrefWindow, "static_element_bool").checked, + string: GetXULElement(aPrefWindow, "static_element_string").value, + unichar: GetXULElement(aPrefWindow, "static_element_unichar").value, + wstring_data: GetXULElement(aPrefWindow, "static_element_wstring").value, + file_data: GetXULElement(aPrefWindow, "static_element_file").value, + wstring: Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ), + file: Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile), + }; + result.wstring.data = result.wstring_data; + SafeFileInit(result.file, result.file_data); + return result; + } + + function RunInstantPrefTest(aPrefWindow) { + // remark: there's currently no UI element binding for files + + // were all Preference instances correctly initialized? + var expected = kPrefValueSet1; + var found = ReadPrefsFromPreferences(aPrefWindow); + ok(found.int === expected.int, "instant pref init int"); + ok(found.bool === expected.bool, "instant pref init bool"); + ok(found.string === expected.string, "instant pref init string"); + ok(found.unichar === expected.unichar, "instant pref init unichar"); + ok(found.wstring_data === expected.wstring_data, "instant pref init wstring"); + todo(found.file_data === expected.file_data, "instant pref init file"); + + // were all elements correctly initialized? (loose check) + found = ReadPrefsFromUI(aPrefWindow); + is(found.int, "" + expected.int, "instant element init int"); + is(found.bool, expected.bool, "instant element init bool"); + is(found.string, expected.string, "instant element init string"); + is(found.unichar, expected.unichar, "instant element init unichar"); + is( + found.wstring_data, expected.wstring_data, + "instant element init wstring" + ); + todo_is(found.file_data == expected.file_data, "instant element init file"); + + // do some changes in the UI + expected = kPrefValueSet2; + WritePrefsToUI(aPrefWindow, expected); + + // UI changes should get passed to the Preference instances, + // but currently they aren't if the changes are made programmatically + // (the handlers preference.change/prefpane.input and prefpane.change + // are called for manual changes, though). + found = ReadPrefsFromPreferences(aPrefWindow); + todo(found.int === expected.int, "instant change pref int"); + todo(found.bool === expected.bool, "instant change pref bool"); + todo(found.string === expected.string, "instant change pref string"); + todo(found.unichar === expected.unichar, "instant change pref unichar"); + todo( + found.wstring_data === expected.wstring_data, + "instant change pref wstring" + ); + todo(found.file_data === expected.file_data, "instant change pref file"); + + // and these changes should get passed to the system instantly + // (which obviously can't pass with the above failing) + found = ReadPrefsFromSystem(); + todo(found.int === expected.int, "instant change element int"); + todo(found.bool === expected.bool, "instant change element bool"); + todo(found.string === expected.string, "instant change element string"); + todo(found.unichar === expected.unichar, "instant change element unichar"); + todo( + found.wstring_data === expected.wstring_data, + "instant change element wstring" + ); + todo(found.file_data === expected.file_data, "instant change element file"); + + // try resetting the prefs to default values (which should be empty here) + GetPreference(aPrefWindow, "tests.static_preference_int").reset(); + GetPreference(aPrefWindow, "tests.static_preference_bool").reset(); + GetPreference(aPrefWindow, "tests.static_preference_string").reset(); + GetPreference(aPrefWindow, "tests.static_preference_unichar").reset(); + GetPreference(aPrefWindow, "tests.static_preference_wstring").reset(); + GetPreference(aPrefWindow, "tests.static_preference_file").reset(); + + // check system + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "instant reset system int"); + ok(found.bool === expected.bool, "instant reset system bool"); + ok(found.string === expected.string, "instant reset system string"); + ok(found.unichar === expected.unichar, "instant reset system unichar"); + ok( + found.wstring_data === expected.wstring_data, + "instant reset system wstring" + ); + ok(found.file_data === expected.file_data, "instant reset system file"); + + // check UI + expected = { + // alas, we don't have XUL elements with typeof(value) == int :( + // int: 0, + int: "", + bool: false, + string: "", + unichar: "", + wstring_data: "", + file_data: "", + wstring: {}, + file: {}, + }; + found = ReadPrefsFromUI(aPrefWindow); + ok(found.int === expected.int, "instant reset element int"); + ok(found.bool === expected.bool, "instant reset element bool"); + ok(found.string === expected.string, "instant reset element string"); + ok(found.unichar === expected.unichar, "instant reset element unichar"); + ok( + found.wstring_data === expected.wstring_data, + "instant reset element wstring" + ); + ok(found.file_data === expected.file_data, "instant reset element file"); + + // check hasUserValue + ok( + !GetPreference(aPrefWindow, "tests.static_preference_int").hasUserValue, + "instant reset hasUserValue int" + ); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_bool").hasUserValue, + "instant reset hasUserValue bool" + ); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_string").hasUserValue, + "instant reset hasUserValue string" + ); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_unichar").hasUserValue, + "instant reset hasUserValue unichar" + ); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_wstring").hasUserValue, + "instant reset hasUserValue wstring" + ); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_file").hasUserValue, + "instant reset hasUserValue file" + ); + } + + function RunCheckCommandRedirect(aPrefWindow) { + ok( + GetPreference(aPrefWindow, "tests.static_preference_bool").value, + "redirected command bool" + ); + GetXULElement(aPrefWindow, "checkbox").click(); + ok( + !GetPreference(aPrefWindow, "tests.static_preference_bool").value, + "redirected command bool" + ); + GetXULElement(aPrefWindow, "checkbox").click(); + ok( + GetPreference(aPrefWindow, "tests.static_preference_bool").value, + "redirected command bool" + ); + } + + function RunCheckDisabled(aPrefWindow) { + ok( + !GetXULElement(aPrefWindow, "disabled_checkbox").disabled, + "Checkbox should be enabled" + ); + GetPreference( + aPrefWindow, + "tests.disabled_preference_bool" + ).updateControlDisabledState(true); + ok( + GetXULElement(aPrefWindow, "disabled_checkbox").disabled, + "Checkbox should be disabled" + ); + GetPreference( + aPrefWindow, + "tests.locked_preference_bool" + ).updateControlDisabledState(false); + ok( + GetXULElement(aPrefWindow, "locked_checkbox").disabled, + "Locked checkbox should stay disabled" + ); + SimpleTest.finish(); + } + + function RunResetPrefTest(aPrefWindow) { + // try resetting the prefs to default values + GetPreference(aPrefWindow, "tests.static_preference_int").reset(); + GetPreference(aPrefWindow, "tests.static_preference_bool").reset(); + GetPreference(aPrefWindow, "tests.static_preference_string").reset(); + GetPreference(aPrefWindow, "tests.static_preference_unichar").reset(); + GetPreference(aPrefWindow, "tests.static_preference_wstring").reset(); + GetPreference(aPrefWindow, "tests.static_preference_file").reset(); + } + + function RunTestApplyPref() { + // Test in parent window. + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences.xhtml", + "", + "modal", + RunInstantPrefTest, + false + ); + + // Test deferred reset in child window. + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences2.xhtml", + "", + "modal", + RunResetPrefTest, + false + ); + let expected = kPrefValueSet1; + let found = ReadPrefsFromSystem(); + is(found.int, expected.int, "instant reset deferred int"); + is(found.bool, expected.bool, "instant reset deferred bool"); + is(found.string, expected.string, "instant reset deferred string"); + is(found.unichar, expected.unichar, "instant reset deferred unichar"); + is( + found.wstring_data, expected.wstring_data, + "instant reset deferred wstring" + ); + todo_is(found.file_data, expected.file_data, "instant reset deferred file"); + + // Test cancel in child window. + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences2.xhtml", + "", + "modal", + aPrefWindow => WritePrefsToPreferences(aPrefWindow, kPrefValueSet2), + false + ); + expected = kPrefValueSet1; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant cancel system int"); + ok(found.bool === expected.bool, "non-instant cancel system bool"); + ok(found.string === expected.string, "non-instant cancel system string"); + ok(found.unichar === expected.unichar, "non-instant cancel system unichar"); + ok( + found.wstring_data === expected.wstring_data, + "non-instant cancel system wstring" + ); + todo( + found.file_data === expected.file_data, + "non-instant cancel system file" + ); + + // Test accept in child window. + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences2.xhtml", + "", + "modal", + aPrefWindow => WritePrefsToPreferences(aPrefWindow, kPrefValueSet2), + true + ); + expected = kPrefValueSet2; + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant accept system int"); + ok(found.bool === expected.bool, "non-instant accept system bool"); + ok(found.string === expected.string, "non-instant accept system string"); + ok(found.unichar === expected.unichar, "non-instant accept system unichar"); + ok( + found.wstring_data === expected.wstring_data, + "non-instant accept system wstring" + ); + todo( + found.file_data === expected.file_data, + "non-instant accept system file" + ); + + // Test deferred reset in child window. + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences2.xhtml", + "", + "modal", + RunResetPrefTest, + true + ); + expected = CreateEmptyPrefValueSet(); + found = ReadPrefsFromSystem(); + ok(found.int === expected.int, "non-instant reset deferred int"); + ok(found.bool === expected.bool, "non-instant reset deferred bool"); + ok(found.string === expected.string, "non-instant reset deferred string"); + ok(found.unichar === expected.unichar, "non-instant reset deferred unichar"); + ok( + found.wstring_data === expected.wstring_data, + "non-instant reset deferred wstring" + ); + ok(found.file_data === expected.file_data, "non-instant reset deferred file"); + } + + function RunTestCommandRedirect() { + WritePrefsToSystem(kPrefValueSet1); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences_commandretarget.xhtml", + "", + "modal", + RunCheckCommandRedirect, + true + ); + } + + function RunTestDisabled() { + // Because this pref is on the default branch and locked, we need to set it before opening the dialog. + const defaultBranch = kPref.getDefaultBranch(""); + defaultBranch.setBoolPref("tests.locked_preference_bool", true); + defaultBranch.lockPref("tests.locked_preference_bool"); + window.browsingContext.topChromeWindow.openDialog( + "window_preferences_disabled.xhtml", + "", + "modal", + RunCheckDisabled, + true + ); + } + + function RunTest() { + RunTestApplyPref(); + RunTestCommandRedirect(); + RunTestDisabled(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml new file mode 100644 index 0000000000..cde1982c0d --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_beforeaccept.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + SimpleTest.registerCleanupFunction(() => { + SpecialPowers.clearUserPref("tests.beforeaccept.dialogShown"); + SpecialPowers.clearUserPref("tests.beforeaccept.called"); + }); + + // No instant-apply for this test because type="child". + var prefWindow = window.browsingContext.topChromeWindow.openDialog( + "window_preferences_beforeaccept.xhtml", + "", + "", + windowOnload + ); + + function windowOnload() { + var dialogShown = prefWindow.Preferences.get( + "tests.beforeaccept.dialogShown" + ); + var called = prefWindow.Preferences.get("tests.beforeaccept.called"); + is(dialogShown.value, true, "dialog opened, shown pref set"); + is(dialogShown.valueFromPreferences, null, "shown pref not committed"); + is(called.value, null, "beforeaccept not yet called"); + is( + called.valueFromPreferences, + null, + "beforeaccept not yet called, pref not committed" + ); + + // try to accept the dialog, should fail the first time + prefWindow.document.getElementById("beforeaccept_dialog").acceptDialog(); + is(prefWindow.closed, false, "window not closed"); + is(dialogShown.value, true, "shown pref still set"); + is(dialogShown.valueFromPreferences, null, "shown pref still not committed"); + is(called.value, true, "beforeaccept called"); + is(called.valueFromPreferences, null, "called pref not committed"); + + // try again, this one should succeed + prefWindow.document.getElementById("beforeaccept_dialog").acceptDialog(); + is(prefWindow.closed, true, "window now closed"); + is(dialogShown.valueFromPreferences, true, "shown pref committed"); + is(called.valueFromPreferences, true, "called pref committed"); + + SimpleTest.finish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml new file mode 100644 index 0000000000..b3c618eac0 --- /dev/null +++ b/toolkit/content/tests/chrome/test_preferences_onsyncfrompreference.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<window title="Preferences Window beforeaccept Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <script type="application/javascript"> + <![CDATA[ + const PREFS = ['tests.onsyncfrompreference.pref1', + 'tests.onsyncfrompreference.pref2', + 'tests.onsyncfrompreference.pref3']; + + SimpleTest.waitForExplicitFinish(); + + for (let pref of PREFS) { + Services.prefs.setIntPref(pref, 1); + } + + let counter = 0; + let prefWindow = window.browsingContext.topChromeWindow.openDialog("window_preferences_onsyncfrompreference.xhtml", "", "", onSync); + + SimpleTest.registerCleanupFunction(() => { + for (let pref of PREFS) { + Services.prefs.clearUserPref(pref); + } + prefWindow.close(); + }); + + // Onsyncfrompreference handler for the prefs + function onSync() { + for (let pref of PREFS) { + // The `value` field of each <preference> element should be initialized by now. + + is(Services.prefs.getIntPref(pref), prefWindow.Preferences.get(pref).value, + "Pref constructor was called correctly") + } + + counter++; + + if (counter == PREFS.length) { + SimpleTest.finish(); + } + return true; + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + </body> + +</window> diff --git a/toolkit/content/tests/chrome/test_props.xhtml b/toolkit/content/tests/chrome/test_props.xhtml new file mode 100644 index 0000000000..6fcee90270 --- /dev/null +++ b/toolkit/content/tests/chrome/test_props.xhtml @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for basic properties - this test checks that the basic + properties defined in general.js and inherited by a number of elements + work properly. + --> +<window title="Basic Properties Test" + onload="setTimeout(test_props, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<command id="cmd_nothing"/> +<command id="cmd_action"/> + +<button id="button" label="Button" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<checkbox id="checkbox" label="Checkbox" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +<radiogroup> + <radio id="radio" label="Radio Button" value="rb1" accesskey="B" + crop="end" image="happy.png" command="cmd_nothing"/> +</radiogroup> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_props() +{ + test_props_forelement($("button"), "Button", null); + test_props_forelement($("checkbox"), "Checkbox", null); + test_props_forelement($("radio"), "Radio Button", "rb1"); + + SimpleTest.finish(); +} + +function test_props_forelement(element, label, value) +{ + // check the initial values + is(element.label, label, "element label"); + if (value) + is(element.value, value, "element value"); + is(element.accessKey, "B", "element accessKey"); + is(element.image, "happy.png", "element image"); + is(element.command, "cmd_nothing", "element command"); + ok(element.tabIndex === 0, "element tabIndex"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_nothing"), "command", "element"); + + // make sure that setters return the value + is(element.label = "Label", "Label", "element label setter return"); + if (value) + is(element.value = "lb", "lb", "element value setter return"); + is(element.accessKey = "L", "L", "element accessKey setter return"); + is(element.image = "sad.png", "sad.png", "element image setter return"); + is(element.command = "cmd_action", "cmd_action", "element command setter return"); + + // check the value after it was changed + is(element.label, "Label", "element label after set"); + if (value) + is(element.value, "lb", "element value after set"); + is(element.accessKey, "L", "element accessKey after set"); + is(element.image, "sad.png", "element image after set"); + is(element.command, "cmd_action", "element command after set"); + + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "command", "element"); + + // check that clicks on disabled items don't fire a command event + // eslint-disable-next-line no-constant-binary-expression + ok((element.disabled = true) === true, "element disabled setter return"); + ok(element.disabled === true, "element disabled after set"); + synthesizeMouseExpectEvent(element, 4, 4, { }, $("cmd_action"), "!command", "element"); + + element.disabled = false; +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_radio.xhtml b/toolkit/content/tests/chrome/test_radio.xhtml new file mode 100644 index 0000000000..3f222a8daa --- /dev/null +++ b/toolkit/content/tests/chrome/test_radio.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for radio buttons + --> +<window title="Radio Buttons" width="500" height="600" + onload="setTimeout(test_radio, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<radiogroup id="radiogroup"/> + +<radiogroup id="radiogroup-initwithvalue" value="two"> + <radio label="One" value="one"/> + <radio label="Two" value="two"/> + <radio label="Three" value="three"/> +</radiogroup> +<radiogroup id="radiogroup-initwithselected" value="two"> + <radio id="one" label="One" value="one" accesskey="o"/> + <radio id="two" label="Two" value="two" accesskey="t"/> + <radio label="Three" value="three" selected="true"/> +</radiogroup> + +<radiogroup id="radio-creation" hidden="true" /> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +async function test_radio() +{ + var element = document.getElementById("radiogroup"); + test_nsIDOMXULSelectControlElement(element, "radio", null); + test_nsIDOMXULSelectControlElement_UI(element, null); + + window.blur(); + + var accessKeyDetails = (navigator.platform.includes("Mac")) ? + { altKey : true, ctrlKey : true } : + { altKey : true, shiftKey: true }; + synthesizeKey("t", accessKeyDetails); + + var radiogroup = $("radiogroup-initwithselected"); + is(document.activeElement, radiogroup, "accesskey focuses radiogroup"); + is(radiogroup.selectedItem, $("two"), "accesskey selects radio"); + + $("radiogroup-initwithvalue").focus(); + + $("one").disabled = true; + synthesizeKey("o", accessKeyDetails); + + is(document.activeElement, $("radiogroup-initwithvalue"), "accesskey on disabled radio doesn't focus"); + is(radiogroup.selectedItem, $("two"), "accesskey on disabled radio doesn't change selection"); + + info("Testing appending child"); + var dynamicRadiogroup = document.querySelector("#radio-creation"); + var radio = document.createXULElement("radio"); + radio.setAttribute("selected", "true"); + radio.setAttribute("label", "one"); + radio.setAttribute("value", "one"); + dynamicRadiogroup.appendChild(radio); + dynamicRadiogroup.appendChild(document.createXULElement("radio")); + + dynamicRadiogroup.hidden = false; + info("Waiting for condition"); + await SimpleTest.promiseWaitForCondition(() => dynamicRadiogroup.value == "one", + "Value gets set once child is constructed"); + is(dynamicRadiogroup._radioChildren.length, 2, "Correct number of children"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_richlistbox.xhtml b/toolkit/content/tests/chrome/test_richlistbox.xhtml new file mode 100644 index 0000000000..48303e0172 --- /dev/null +++ b/toolkit/content/tests/chrome/test_richlistbox.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for listbox direction + --> +<window title="Listbox direction test" + onload="test_richlistbox()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <richlistbox seltype="multiple" id="richlistbox" flex="1" style="min-height: 80px; max-height: 80px; height: 80px"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var richListBox = document.getElementById("richlistbox"); + +function getScrollIndexAmount(aDirection) { + return (4 * aDirection + richListBox.currentIndex); +} + +function test_richlistbox() +{ + var height = richListBox.clientHeight; + var item; + do { + item = richListBox.appendItem("Test", ""); + item.style.height = item.style.minHeight = item.style.maxHeight = Math.floor(height / 4) + "px"; + } while (item.getBoundingClientRect().bottom < (height * 2)) + richListBox.appendItem("Test", ""); + richListBox.firstChild.nextSibling.id = "list-box-first"; + richListBox.lastChild.previousSibling.id = "list-box-last"; + + var count = richListBox.itemCount; + richListBox.focus(); + + // Test that dir="reverse" is ignored and behaves the same as dir="normal". + for (let dir of ["reverse", "normal"]) { + richListBox.style.MozBoxDirection = dir; + richListBox.selectedIndex = 0; + sendKey("DOWN"); + is(richListBox.currentIndex, 1, "Selection should move to the next item"); + sendKey("UP"); + is(richListBox.currentIndex, 0, "Selection should move to the previous item"); + sendKey("END"); + is(richListBox.currentIndex, count - 1, "Selection should move to the last item"); + sendKey("HOME"); + is(richListBox.currentIndex, 0, "Selection should move to the first item"); + var currentIndex = richListBox.currentIndex; + var index = richListBox.scrollOnePage(1); + sendKey("PAGE_DOWN"); + is(richListBox.currentIndex, index, "Selection should move to one page down"); + ok(richListBox.currentIndex > currentIndex, "Selection should move downwards"); + sendKey("END"); + currentIndex = richListBox.currentIndex; + index = richListBox.scrollOnePage(-1) + richListBox.currentIndex; + sendKey("PAGE_UP"); + is(richListBox.currentIndex, index, "Selection should move to one page up"); + ok(richListBox.currentIndex < currentIndex, "Selection should move upwards"); + richListBox.selectedItem = richListBox.firstChild; + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", {shiftKey: true}, window); + let items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.firstChild; + sendMouseEvent({type: "click", shiftKey: true, clickCount: 1}, + "list-box-first", + window); + items = [richListBox.selectedItems[0], + richListBox.selectedItems[1]]; + is(items[0], richListBox.firstChild, "The last element should still be selected"); + is(items[1], richListBox.firstChild.nextSibling, "Both elements should now be selected"); + richListBox.addEventListener("keypress", function(aEvent) { + aEvent.preventDefault(); + }, { useCapture: true, once: true }); + richListBox.selectedIndex = 1; + sendKey("HOME"); + is(richListBox.selectedIndex, 1, "A stopped event should return indexing to normal"); + } + + // Test attempting to select a disabled item. + richListBox.clearSelection(); + richListBox.selectedItem = richListBox.firstChild; + richListBox.firstChild.nextSibling.setAttribute("disabled", true); + richListBox.focus(); + synthesizeKey("KEY_ArrowDown", {}, window); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], richListBox.firstChild, "first item selected"); + + // Selected item re-insertion should keep the item selected. + richListBox.clearSelection(); + item = richListBox.firstElementChild; + richListBox.selectedItem = item; + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], item, "first item selected"); + item.remove(); + richListBox.append(item); + is(richListBox.selectedItems.length, 1, "one item selected"); + is(richListBox.selectedItems[0], item, "last (previosly first) item selected"); + + SimpleTest.finish(); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_screenPersistence.xhtml b/toolkit/content/tests/chrome/test_screenPersistence.xhtml new file mode 100644 index 0000000000..667dcf9590 --- /dev/null +++ b/toolkit/content/tests/chrome/test_screenPersistence.xhtml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + let win; + var left = 60 + screen.availLeft; + var upper = 60 + screen.availTop; + + function runTest() { + win = window.browsingContext.topChromeWindow + .openDialog("window_screenPosSize.xhtml", + "_blank", + "chrome,dialog=no,all,screenX=" + left + ",screenY=" + upper + ",outerHeight=200,outerWidth=200"); + SimpleTest.waitForFocus(checkTest, win); + } + function checkTest() { + is(win.screenX, left, "The window should be placed now at x=" + left + "px"); + is(win.screenY, upper, "The window should be placed now at y=" + upper + "px"); + is(win.outerHeight, 200, "The window size should be height=200px"); + is(win.outerWidth, 200, "The window size should be width=200px"); + runTest2(); + } + function runTest2() { + win.close(); + win = window.browsingContext.topChromeWindow + .openDialog("window_screenPosSize.xhtml", + "_blank", + "chrome,dialog=no,all"); + SimpleTest.waitForFocus(checkTest2, win); + } + function checkTest2() { + let runTime = SpecialPowers.Services.appinfo; + if (runTime.OS != "Linux") { + is(win.screenX, 80, "The window should be placed now at x=80px"); + is(win.screenY, 80, "The window should be placed now at y=80px"); + } + is(win.innerHeight, 300, "The window size should be height=300px"); + is(win.innerWidth, 300, "The window size should be width=300px"); + win.close(); + SimpleTest.finish(); + } +]]></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_scrollbar.xhtml b/toolkit/content/tests/chrome/test_scrollbar.xhtml new file mode 100644 index 0000000000..c16e9f2980 --- /dev/null +++ b/toolkit/content/tests/chrome/test_scrollbar.xhtml @@ -0,0 +1,131 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for scrollbars + --> +<window title="Scrollbar" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"/> + + <hbox> + <scrollbar orient="horizontal" + id="scroller" + curpos="0" + maxpos="600" + pageincrement="400" + style="width: 500px; margin: 0"/> + </hbox> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +/** Test for Scrollbar **/ +var scrollbarTester = { + scrollbar: null, + middlePref: false, + startTest() { + this.scrollbar = $("scroller"); + this.middlePref = this.getMiddlePref(); + var self = this; + [0, 1, 2].map(function(button) { + [false, true].map(function(alt) { + [false, true].map(function(shift) { + self.testThumbDragging(button, alt, shift); + }) + }) + }); + SimpleTest.finish(); + }, + testThumbDragging(button, withAlt, withShift) { + this.reset(); + var x = 160; // on the right half of the thumb + var y = 5; + + var isMac = navigator.platform.includes("Mac"); + let runtime = SpecialPowers.Services.appinfo; + var isGtk = runtime.widgetToolkit.includes("gtk"); + + // Start the drag. + this.mousedown(x, y, button, withAlt, withShift); + var newPos = this.getPos(); + var scrollToClick = (newPos != 0); + if (isMac || isGtk) { + ok(!scrollToClick, "On Linux and Mac OS X, clicking the scrollbar thumb "+ + "should never move it."); + } else if (button == 0 && withShift) { + ok(scrollToClick, "On platforms other than Linux and Mac OS X, holding "+ + "shift should enable scroll-to-click on the scrollbar thumb."); + } else if (button == 1 && this.middlePref) { + ok(scrollToClick, "When middlemouse.scrollbarPosition is on, clicking the "+ + "thumb with the middle mouse button should center it "+ + "around the cursor.") + } + + // Move one pixel to the right. + this.mousemove(x+1, y, button, withAlt, withShift); + var newPos2 = this.getPos(); + if (newPos2 != newPos) { + ok(newPos2 > newPos, "Scrollbar thumb should follow the mouse when dragged."); + ok(newPos2 - newPos < 3, "Scrollbar shouldn't move further than the mouse when dragged."); + ok(button == 0 || (button == 1 && this.middlePref) || (button == 2 && isGtk), + "Dragging the scrollbar should only be possible with the left mouse button."); + } else if (button == 0) { + // Dragging had no effect. + ok(false, "Dragging the scrollbar thumb should work."); + } else if (button == 1 && this.middlePref && (!isGtk && !isMac)) { + ok(false, "When middlemouse.scrollbarPosition is on, dragging the "+ + "scrollbar thumb should be possible using the middle mouse button."); + } else { + ok(true, "Dragging works correctly."); + } + + // Release the mouse button. + this.mouseup(x+1, y, button, withAlt, withShift); + var newPos3 = this.getPos(); + ok(newPos3 == newPos2, + "Releasing the mouse button after dragging the thumb shouldn't move it."); + }, + getMiddlePref() { + // It would be better to test with different middlePref settings, + // but the setting is only queried once, at browser startup, so + // changing it here wouldn't have any effect + var mouseBranch = SpecialPowers.Services.prefs.getBranch("middlemouse."); + return mouseBranch.getBoolPref("scrollbarPosition"); + }, + setPos(pos) { + this.scrollbar.setAttribute("curpos", pos); + }, + getPos() { + return this.scrollbar.getAttribute("curpos"); + }, + reset() { + this.setPos(0); + }, + mousedown(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousedown", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mousemove(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mousemove", 'button': button, + altKey: alt, shiftKey: shift }); + }, + mouseup(x, y, button, alt, shift) { + synthesizeMouse(this.scrollbar, x, y, { type: "mouseup", 'button': button, + altKey: alt, shiftKey: shift }); + } +} + +function doTest() { + setTimeout(function() { scrollbarTester.startTest(); }, 0); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/test_showcaret.xhtml b/toolkit/content/tests/chrome/test_showcaret.xhtml new file mode 100644 index 0000000000..eb344589fc --- /dev/null +++ b/toolkit/content/tests/chrome/test_showcaret.xhtml @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Show Caret Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<iframe id="f1" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style='height:%208000px'%3E%3Cp%3EHello%3C/p%3EGoodbye%3C/body%3E"/> +<!-- <body style='height: 8000px'><p>Hello</p><span id='s'>Goodbye<span></body> --> +<iframe id="f2" type="content" showcaret="true" width="100" height="100" onload="frameLoaded()" + src="data:text/html,%3Cbody%20style%3D%27height%3A%208000px%27%3E%3Cp%3EHello%3C%2Fp%3E%3Cspan%20id%3D%27s%27%3EGoodbye%3Cspan%3E%3C%2Fbody%3E"/> + +<script> +<![CDATA[ + +var framesLoaded = 0; +var otherWindow = null; + +function frameLoaded() { if (++framesLoaded == 2) SimpleTest.waitForFocus(runTest); } + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + var sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + var sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + var listener = function() { + if (!(frames[0].scrollY > 0)) { + window.content.removeEventListener("scroll", listener); + } + } + window.frames[0].addEventListener("scroll", listener); + + sel1 = frames[0].getSelection(); + sel1.collapse(frames[0].document.body, 0); + + sel2 = frames[1].getSelection(); + sel2.collapse(frames[1].document.body, 0); + + window.frames[0].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + is(sel1.focusNode, frames[0].document.body, "focusNode for non-showcaret"); + is(sel1.focusOffset, 0, "focusOffset for non-showcaret"); + + window.frames[1].focus(); + document.commandDispatcher.getControllerForCommand("cmd_moveBottom").doCommand("cmd_moveBottom"); + + ok(frames[1].scrollY < + frames[1].document.getElementById('s').getBoundingClientRect().top, + "scrollY for showcaret"); + isnot(sel2.focusNode, frames[1].document.body, "focusNode for showcaret"); + ok(sel2.anchorOffset > 0, "focusOffset for showcaret"); + + otherWindow = window.browsingContext.topChromeWindow.open("window_showcaret.xhtml", "_blank", "chrome,width=400,height=200"); + otherWindow.addEventListener("focus", otherWindowFocused); +} + +function otherWindowFocused() +{ + otherWindow.removeEventListener("focus", otherWindowFocused); + + // enable caret browsing temporarily to test caret movement + let prefs = SpecialPowers.Services.prefs; + prefs.setBoolPref("accessibility.browsewithcaret", true); + + var hbox = otherWindow.document.documentElement.firstChild; + hbox.focus(); + is(otherWindow.document.activeElement, hbox, "hbox in other window is focused"); + + document.commandDispatcher.getControllerForCommand("cmd_lineNext").doCommand("cmd_lineNext"); + is(otherWindow.document.activeElement, hbox, "hbox still focused in other window after down movement"); + + prefs.setBoolPref("accessibility.browsewithcaret", false); + + otherWindow.close(); + SimpleTest.finish(); +} + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_subframe_origin.xhtml b/toolkit/content/tests/chrome/test_subframe_origin.xhtml new file mode 100644 index 0000000000..65b7190baf --- /dev/null +++ b/toolkit/content/tests/chrome/test_subframe_origin.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Subframe Event Tests" + onload="setTimeout(runTest, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<script> + +// Added after content child widgets were removed from ui windows. Tests sub frame +// event client coordinate offsets. + +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_subframe_origin.xhtml", "_blank", "chrome,width=600,height=600,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabbox.xhtml b/toolkit/content/tests/chrome/test_tabbox.xhtml new file mode 100644 index 0000000000..bca919f8c5 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabbox.xhtml @@ -0,0 +1,223 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabboxes + --> +<window title="Tabbox Test" width="500" height="600" + onload="setTimeout(test_tabbox, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="xul_selectcontrol.js"/> + +<vbox id="tabboxes"> + +<tabbox id="tabbox"> + <tabs id="tabs"> + <tab id="tab1" label="Tab 1"/> + <tab id="tab2" label="Tab 2"/> + </tabs> + <tabpanels id="tabpanels"> + <button id="panel1" label="Panel 1"/> + <button id="panel2" label="Panel 2"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithvalue"> + <tabs id="tabs-initwithvalue" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three"/> + </tabs> + <tabpanels id="tabpanels-initwithvalue"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +<tabbox id="tabbox-initwithselected"> + <tabs id="tabs-initwithselected" value="two"> + <tab label="Tab 1" value="one"/> + <tab label="Tab 2" value="two"/> + <tab label="Tab 3" value="three" selected="true"/> + </tabs> + <tabpanels id="tabpanels-initwithselected"> + <button label="Panel 1"/> + <button label="Panel 2"/> + <button label="Panel 3"/> + </tabpanels> +</tabbox> + +</vbox> + +<tabbox id="tabbox-nofocus"> + <html:input id="textbox-extra" hidden="true"/> + <tabs> + <tab label="Tab 1" value="one"/> + <tab id="tab-nofocus" label="Tab 2" value="two"/> + </tabs> + <tabpanels> + <tabpanel> + <button id="tab-nofocus-button" label="Label"/> + </tabpanel> + <tabpanel id="tabpanel-nofocusinpaneltab"> + <label id="tablabel" value="Label"/> + </tabpanel> + </tabpanels> +</tabbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function test_tabbox() +{ + var tabbox = document.getElementById("tabbox"); + var tabs = document.getElementById("tabs"); + var tabpanels = document.getElementById("tabpanels"); + + test_tabbox_State(tabbox, "tabbox initial", 0, tabs.allTabs[0], tabpanels.firstChild); + + // check the selectedIndex property + tabbox.selectedIndex = 1; + test_tabbox_State(tabbox, "tabbox selectedIndex 1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + tabbox.selectedIndex = 2; + test_tabbox_State(tabbox, "tabbox selectedIndex 2", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + // tabbox must have a selection, so setting to -1 should do nothing + tabbox.selectedIndex = -1; + test_tabbox_State(tabbox, "tabbox selectedIndex -1", 1, tabs.allTabs[tabs.allTabs.length - 1], tabpanels.lastChild); + + // check the selectedTab property + tabbox.selectedTab = tabs.allTabs[0]; + test_tabbox_State(tabbox, "tabbox selected", 0, tabs.allTabs[0], tabpanels.firstChild); + + // setting selectedTab to null should not do anything + tabbox.selectedTab = null; + test_tabbox_State(tabbox, "tabbox selectedTab null", 0, tabs.allTabs[0], tabpanels.firstChild); + + // check the selectedPanel property + tabbox.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabbox selectedPanel", 0, tabs.allTabs[0], tabpanels.lastChild); + + // setting selectedPanel to null should not do anything + tabbox.selectedPanel = null; + test_tabbox_State(tabbox, "tabbox selectedPanel null", 0, tabs.allTabs[0], tabpanels.lastChild); + + tabbox.selectedIndex = 0; + test_tabpanels(tabpanels, tabbox); + + tabs.firstChild.remove(); + tabs.firstChild.remove(); + + test_tabs(tabs); + + test_tabbox_focus(); +} + +function test_tabpanels(tabpanels, tabbox) +{ + var tab = tabbox.selectedTab; + + // changing the selection on the tabpanels should not affect the tabbox + // or tabs within + // check the selectedIndex property + tabpanels.selectedIndex = 1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 1", 1, tabpanels.lastChild); + + tabpanels.selectedIndex = 0; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex 2", 0, tab, tabpanels.firstChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex 2", 0, tabpanels.firstChild); + + // setting selectedIndex to -1 should do nothing + tabpanels.selectedIndex = 1; + tabpanels.selectedIndex = -1; + test_tabbox_State(tabbox, "tabpanels tabbox selectedIndex -1", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedIndex -1", 1, tabpanels.lastChild); + + // check the tabpanels.selectedPanel property + tabpanels.selectedPanel = tabpanels.lastChild; + test_tabbox_State(tabbox, "tabpanels tabbox selectedPanel", 0, tab, tabpanels.lastChild); + test_tabpanels_State(tabpanels, "tabpanels selectedPanel", 1, tabpanels.lastChild); + + // check setting the tabpanels.selectedPanel property to null + tabpanels.selectedPanel = null; + test_tabbox_State(tabbox, "tabpanels selectedPanel null", 0, tab, tabpanels.lastChild); +} + +function test_tabs(tabs) +{ + test_nsIDOMXULSelectControlElement(tabs, "tab", "tabs"); + // XXXndeakin would test the UI aspect of tabs, but the mouse + // events on tabs are fired in a timeout causing the generic + // test_nsIDOMXULSelectControlElement_UI method not to work + // test_nsIDOMXULSelectControlElement_UI(tabs, null); +} + +function test_tabbox_State(tabbox, testid, index, tab, panel) +{ + is(tabbox.selectedIndex, index, testid + " selectedIndex"); + is(tabbox.selectedTab, tab, testid + " selectedTab"); + is(tabbox.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabpanels_State(tabpanels, testid, index, panel) +{ + is(tabpanels.selectedIndex, index, testid + " selectedIndex"); + is(tabpanels.selectedPanel, panel, testid + " selectedPanel"); +} + +function test_tabbox_focus() +{ + $("tabboxes").hidden = true; + $(document.activeElement).blur(); + + var tabbox = $("tabbox-nofocus"); + var tab = $("tab-nofocus"); + + when_tab_focused(tab, function () { + is(document.activeElement, tab, "focus in tab with no focusable elements"); + + tabbox.selectedIndex = 0; + $("tab-nofocus-button").focus(); + + when_tab_focused(tab, function () { + is(document.activeElement, tab, "focus in tab with no focusable elements, but with something in another tab focused"); + + var textboxExtra = $("textbox-extra"); + textboxExtra.addEventListener("focus", function () { + is(document.activeElement, textboxExtra, "focus in tab with focus currently in textbox that is sibling of tabs"); + + SimpleTest.finish(); + }, {once: true}); + + tabbox.selectedIndex = 0; + textboxExtra.hidden = false; + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); + }); + + synthesizeMouseAtCenter(tab, { }); +} + +function when_tab_focused(tab, callback) { + tab.addEventListener("focus", function onFocused() { + SimpleTest.executeSoon(callback); + }, {once: true}); +} + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tabindex.xhtml b/toolkit/content/tests/chrome/test_tabindex.xhtml new file mode 100644 index 0000000000..9828728c63 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tabindex.xhtml @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tabindex + --> +<window title="tabindex" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<!-- + Elements are navigated in the following order: + 1. tabindex > 0 in tree order + 2. tabindex = 0 in tree order + Elements with tabindex = -1 are focusable, but not in the tab order + --> +<hbox> + <button id="t7" label="One"/> + <checkbox id="f1" label="Two" tabindex="-1"/> + <button id="t8" label="Three" tabindex="0"/> + <checkbox id="t1" label="Four" tabindex="1"/> +</hbox> +<hbox> + <html:input id="t9" idmod="t5" size="3"/> + <html:input id="f2" size="3" tabindex="-1"/> + <html:input id="t10" idmod="t6" size="3" tabindex="0"/> + <html:input id="t2" idmod="t1" size="3" tabindex="1"/> +</hbox> +<hbox> + <button id="n1" style="-moz-user-focus: ignore;" label="One"/> + <checkbox id="f3" style="-moz-user-focus: ignore;" label="Two" tabindex="-1"/> + <button id="t11" style="-moz-user-focus: ignore;" label="Three" tabindex="0"/> + <checkbox id="t3" style="-moz-user-focus: ignore;" label="Four" tabindex="1"/> +</hbox> +<hbox> + <html:input id="t12" idmod="t7" style="-moz-user-focus: ignore;" size="3"/> + <html:input id="f4" style="-moz-user-focus: ignore;" size="3" tabindex="-1"/> + <html:input id="t13" idmod="t8" style="-moz-user-focus: ignore;" size="3" tabindex="0"/> + <html:input id="t4" idmod="t2" style="-moz-user-focus: ignore;" size="3" tabindex="1"/> +</hbox> +<richlistbox id="t14" idmod="t9"> + <richlistitem><label value="Item One"/></richlistitem> +</richlistbox> + +<hbox> + <!-- the tabindex attribute applies to non-controls as well. They are not + focusable unless tabindex is explicitly specified. + --> + <dropmarker id="n2"/> + <dropmarker id="f5" tabindex="-1"/> + <dropmarker id="t15" tabindex="0"/> + <dropmarker id="t5" idmod="t3" tabindex="1"/> + <dropmarker id="t16" style="-moz-user-focus: normal;"/> + <dropmarker id="f6" style="-moz-user-focus: normal;" tabindex="-1"/> + <dropmarker id="t17" style="-moz-user-focus: normal;" tabindex="0"/> + <dropmarker id="t6" idmod="t4" style="-moz-user-focus: normal;" tabindex="1"/> +</hbox> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +<script> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkFocusability(aId, aFocusable) +{ + document.activeElement.blur(); + let testNode = document.getElementById(aId); + testNode.focus(); + let newFocus = document.activeElement; + let check = aFocusable ? is : isnot; + let focusableText = aFocusable ? "focusable " : "unfocusable "; + check(newFocus, testNode, + ".focus() call on " + focusableText + aId); +} + +var gAdjustedTabFocusModel = false; +var gTestCount = 17; +var gTestsOccurred = 0; +let gFocusableNotTabbableCount = 6; +let gNotFocusableCount = 2; + +function runTests() +{ + var t; + function onFocus(event) { + if (t == 1 && event.target.id == "t2") { + // looks to be using the MacOSX Full Keyboard Access set to Textboxes + // and lists only so use the idmod attribute instead + gAdjustedTabFocusModel = true; + gTestCount = 9; + } + + var attrcompare = gAdjustedTabFocusModel ? "idmod" : "id"; + + // check for the last test which should wrap aorund to the first item + // consider the focus event on the inner input of textboxes instead + is(event.target.getAttribute(attrcompare), "t" + t, + "tab " + t + " to " + event.target.localName); + gTestsOccurred++; + } + window.addEventListener("focus", onFocus, true); + + for (t = 1; t <= gTestCount; t++) + synthesizeKey("KEY_Tab"); + + is(gTestsOccurred, gTestCount, "test count"); + window.removeEventListener("focus", onFocus, true); + + for (let i = 1; i <= gFocusableNotTabbableCount; ++i) { + checkFocusability("f" + i, true); + } + + for (let i = 1; i <= gNotFocusableCount; ++i) { + checkFocusability("n" + i, false); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +]]> + +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_textbox_search.xhtml b/toolkit/content/tests/chrome/test_textbox_search.xhtml new file mode 100644 index 0000000000..216caae6f4 --- /dev/null +++ b/toolkit/content/tests/chrome/test_textbox_search.xhtml @@ -0,0 +1,175 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for search textbox + --> +<window title="Search textbox test" width="500" height="600" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <hbox id="searchbox-container"> + <search-textbox id="searchbox" + oncommand="doSearch(this.value);" + placeholder="random placeholder" + timeout="1"/> + </hbox> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedValue; +var gLastTest; + +function doTests() { + var textbox = $("searchbox"); + + // Reconnect the searchbox to ensure connectedCallback only runs once + // (bug 1650486). + textbox.remove(); + $("searchbox-container").append(textbox); + is(textbox.shadowRoot.querySelectorAll(".textbox-search-sign").length, 1, + "only one search icon in the search box"); + + var icons = textbox._searchIcons; + var searchIcon = icons.querySelector(".textbox-search-icon"); + var clearIcon = icons.querySelector(".textbox-search-clear"); + + ok(icons, "icon deck found"); + ok(searchIcon, "search icon found"); + ok(clearIcon, "clear icon found"); + is(icons.selectedPanel, searchIcon, "search icon is displayed"); + + is(textbox.placeholder, "random placeholder", "search textbox supports placeholder"); + is(textbox.value, "", "placeholder doesn't interfere with the real value"); + is(textbox.shadowRoot.querySelector("input").getAttribute("inputmode"), "search", "inputmode of search textbox is search by default"); + + function iconClick(aIcon) { + is(icons.selectedPanel, aIcon, aIcon.className + " icon must be displayed in order to be clickable"); + + //XXX synthesizeMouse worked on Linux but failed on Windows an Mac + // for unknown reasons. Manually dispatch the event for now. + //synthesizeMouse(aIcon, 0, 0, {}); + + var event = document.createEvent("MouseEvent"); + event.initMouseEvent("click", true, true, window, 1, + 0, 0, 0, 0, + false, false, false, false, + 0, null); + aIcon.dispatchEvent(event); + } + + iconClick(searchIcon); + is(textbox.getAttribute("focused"), "true", "clicking the search icon focuses the textbox"); + + textbox.value = "foo"; + is(icons.selectedPanel, clearIcon, "clear icon is displayed when setting a value"); + + textbox.value = ""; + is(textbox.defaultValue, "", "defaultValue is empty"); + is(textbox.value, "", "reset method clears the textbox"); + is(icons.selectedPanel, searchIcon, "search icon is displayed after clearing value"); + + textbox.value = "foo"; + gExpectedValue = ""; + iconClick(clearIcon); + is(textbox.value, "", "clicking the clear icon clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the clear icon"); + + textbox.value = "foo"; + gExpectedValue = ""; + synthesizeKey("KEY_Escape"); + is(textbox.value, "", "escape key clears the textbox"); + ok(gExpectedValue == null, "search triggered when clearing the textbox with the escape key"); + + textbox.value = "bar"; + gExpectedValue = "bar"; + textbox.doCommand(); + ok(gExpectedValue == null, "search triggered with doCommand"); + + gExpectedValue = "bar"; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key"); + + textbox.value = ""; + textbox.searchButton = true; + is(textbox.getAttribute("searchbutton"), "true", "searchbutton attribute set on the textbox"); + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode if there's a value"); + + gExpectedValue = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting"); + + sendString("o"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when typing a key"); + + gExpectedValue = "fooo"; + iconClick(searchIcon); // display the clear icon (tested above) + + textbox.value = "foo"; + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when the value is changed"); + + gExpectedValue = "foo"; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, clearIcon, "clear icon displayed in search button mode after submitting with enter key"); + + textbox.value = "x"; + synthesizeKey("KEY_Backspace"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode when deleting the value with the backspace key"); + + gExpectedValue = ""; + synthesizeKey("KEY_Enter"); + ok(gExpectedValue == null, "search triggered with enter key in search button mode"); + is(icons.selectedPanel, searchIcon, "search icon displayed in search button mode after submitting an empty string"); + + textbox.readOnly = true; + gExpectedValue = "foo"; + textbox.value = "foo"; + iconClick(searchIcon); + ok(gExpectedValue == null, "search triggered when clicking the search icon in search button mode while the textbox is read-only"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode after submitting while the textbox is read-only"); + textbox.readOnly = false; + + textbox.disabled = true; + is(searchIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the search icon"); + is(clearIcon.getAttribute("disabled"), "true", "disabled attribute inherited to the clear icon"); + gExpectedValue = false; + textbox.value = "foo"; + iconClick(searchIcon); + ok(!gExpectedValue, "search *not* triggered when clicking the search icon in search button mode while the textbox is disabled"); + is(icons.selectedPanel, searchIcon, "search icon persists in search button mode when trying to submit while the textbox is disabled"); + textbox.disabled = false; + ok(!searchIcon.hasAttribute("disabled"), "disabled attribute removed from the search icon"); + ok(!clearIcon.hasAttribute("disabled"), "disabled attribute removed from the clear icon"); + + textbox.searchButton = false; + ok(!textbox.hasAttribute("searchbutton"), "searchbutton attribute removed from the textbox"); + + gLastTest = true; + gExpectedValue = "123"; + textbox.value = "1"; + sendString("23"); +} + +function doSearch(aValue) { + is(aValue, gExpectedValue, "search triggered with expected value"); + gExpectedValue = null; + if (gLastTest) + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + + ]]></script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip.xhtml b/toolkit/content/tests/chrome/test_tooltip.xhtml new file mode 100644 index 0000000000..ec4855131d --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + onload="setTimeout(runTest, 0)" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function runTest() +{ + window.openDialog("window_tooltip.xhtml", "_blank", "chrome,width=700,height=700,noopener", window); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml b/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml new file mode 100644 index 0000000000..99216809bf --- /dev/null +++ b/toolkit/content/tests/chrome/test_tooltip_noautohide.xhtml @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Noautohide Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<tooltip id="thetooltip" noautohide="true" + onpopupshown="setTimeout(tooltipStillShown, 6000)" + onpopuphidden="ok(gChecked, 'tooltip did not hide'); SimpleTest.finish()"> + <label id="label" value="This is a tooltip"/> +</tooltip> + +<button id="button" label="Tooltip Text" tooltip="thetooltip"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gChecked = false; + +function runTests() +{ + var button = document.getElementById("button"); + var windowUtils = window.windowUtils; + windowUtils.disableNonTestMouseEvents(true); + synthesizeMouse(button, 2, 2, { type: "mouseover" }); + synthesizeMouse(button, 4, 4, { type: "mousemove" }); + synthesizeMouse(button, 6, 6, { type: "mousemove" }); + windowUtils.disableNonTestMouseEvents(false); +} + +function tooltipStillShown() +{ + gChecked = true; + document.getElementById("thetooltip").hidePopup(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree.xhtml b/toolkit/content/tests/chrome/test_tree.xhtml new file mode 100644 index 0000000000..d35d6354ab --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using multiple row selection + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-simple', 'treechildren-simple', 'multiple', 'simple', 'tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-simple" rows="4"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-simple"> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> + diff --git a/toolkit/content/tests/chrome/test_tree_hier.xhtml b/toolkit/content/tests/chrome/test_tree_hier.xhtml new file mode 100644 index 0000000000..fd4198c804 --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_hier.xhtml @@ -0,0 +1,136 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for hierarchical tree + --> +<window title="Hierarchical Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-hier', 'treechildren-hier', 'multiple', '', 'hierarchical tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-hier" rows="4"> + <treecols> + <treecol id="name" label="Name" primary="true" + sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" style="flex-grow: 2; flex-shrink: 2"/> + <treecol id="planet" label="Planet" flex="1"/> + <treecol id="gender" label="Gender" flex="1" cycler="true"/> + </treecols> + <treechildren id="treechildren-hier"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + <treecell label="Earth"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Saturn"/> + <treecell label="Female" value="f"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + <treecell label="Female" value="f"/> + <treecell label="Neptune"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Omicron Persei 8"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + <treecell label=""/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + <treecell label="Neptune"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + <treecell label="Earth"/> + <treecell label="Female" value="f"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + <treecell label="Mars"/> + <treecell label="Male" value="m"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_scroll.xhtml b/toolkit/content/tests/chrome/test_tree_scroll.xhtml new file mode 100644 index 0000000000..08b6d9141a --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_scroll.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for scrolling behavior of tree +--> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree_scroll);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" +> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script> + + <script src="chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <script src="chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> + <script src="tree_shared.js"/> + +<html:div style="height: 200px; overflow-y: scroll;"> + <html:div id="top" style="height: 50px; background: cyan;"></html:div> + <tree rows="3"> + <treecols> + <treecol id="name" label="label" sort="label" flex="1"/> + </treecols> + <treechildren> + <treeitem> + <treerow> + <treecell label="0"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="1"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="2"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="3"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="4"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="5"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="6"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="7"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="8"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="9"/> + </treerow> + </treeitem> + </treechildren> + </tree> + <html:div id="bottom" style="height: 150px; background: orange;"></html:div> +</html:div> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_single.xhtml b/toolkit/content/tests/chrome/test_tree_single.xhtml new file mode 100644 index 0000000000..d5bae73ccb --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_single.xhtml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for single selection tree + --> +<window title="Single Selection Tree" width="500" height="600" + onload="setTimeout(testtag_tree, 0, 'tree-single', 'treechildren-single', + 'single', 'simple', 'single selection tree');" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<tree id="tree-single" rows="4" seltype="single"> + <treecols> + <treecol id="name" label="Name" sort="label" properties="one two" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-single"> + <treeitem> + <treerow properties="firstrow"> + <treecell label="Mary" value="mary" properties="firstname"/> + <treecell label="206 Garden Avenue" value="206ga"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell/> + <treecell value="19ms"/> + </treerow> + </treeitem> + <treeitem container="true"> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + <treechildren> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue" editable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Mary"/> + <treecell label="206 Garden Avenue" selectable="false"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Chris"/> + <treecell label="19 Marion Street"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="Sarah"/> + <treecell label="702 Fern Avenue"/> + </treerow> + </treeitem> + <treeitem> + <treerow> + <treecell label="John"/> + <treecell label="99 Westminster Avenue"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_tree_view.xhtml b/toolkit/content/tests/chrome/test_tree_view.xhtml new file mode 100644 index 0000000000..5e45161c6c --- /dev/null +++ b/toolkit/content/tests/chrome/test_tree_view.xhtml @@ -0,0 +1,113 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for tree using a custom nsITreeView + --> +<window title="Tree" onload="init()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<script src="tree_shared.js"/> + +<script> +<![CDATA[ +/* import-globals-from ../widgets/tree_shared.js */ +// This is our custom view, based on the treeview interface +var view = +{ + treeData: [["Mary", "206 Garden Avenue"], + ["Chris", "19 Marion Street"], + ["Sarah", "702 Fern Avenue"], + ["John", "99 Westminster Avenue"]], + value: "", + rowCount: 8, + getCellText(row, column) { return this.treeData[row % 4][column.index]; }, + getCellValue(row, column) { return this.value; }, + setCellText(row, column, val) { this.treeData[row % 4][column.index] = val; }, + setCellValue(row, column, val) { this.value = val; }, + setTree(tree) { this.tree = tree; }, + isContainer(row) { return false; }, + isContainerOpen(row) { return false; }, + isContainerEmpty(row) { return false; }, + isSeparator(row) { return false; }, + isSorted(row) { return false; }, + isEditable(row, column) { return row != 2 || column.index != 1; }, + getParentIndex(row, column) { return -1; }, + getLevel(row) { return 0; }, + hasNextSibling(row, column) { return row != this.rowCount - 1; }, + getImageSrc(row, column) { return ""; }, + cycleHeader(column) { }, + getRowProperties(row) { return ""; }, + getCellProperties(row, column) { return ""; }, + getColumnProperties(column) + { + if (!column.index) { + return "one two"; + } + + return ""; + } +} + +function getCustomTreeViewCellInfo() +{ + var obj = { rows: [] }; + + for (var row = 0; row < view.rowCount; row++) { + var cellInfo = [ ]; + for (var column = 0; column < 1; column++) { + cellInfo.push({ label: "" + view.treeData[row % 4][column], + value: "", + properties: "", + editable: row != 2 || column.index != 1, + selectable: true, + image: "" }); + } + + obj.rows.push({ cells: cellInfo, + properties: "", + container: false, + separator: false, + children: null, + level: 0, + parent: -1 }); + } + + return obj; +} + +function init() +{ + var tree = document.getElementById("tree-view"); + tree.view = view; + tree.ensureRowIsVisible(0); + is(tree.getFirstVisibleRow(), 0, "first visible after ensureRowIsVisible on load"); + + setTimeout(testtag_tree, 0, "tree-view", "treechildren-view", "multiple", "simple", "tree view"); +} + +]]> +</script> + +<tree id="tree-view" rows="4"> + <treecols> + <treecol id="name" label="Name" sort="label" flex="1"/> + <treecol id="address" label="Address" flex="1"/> + </treecols> + <treechildren id="treechildren-view"/> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml b/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml new file mode 100644 index 0000000000..451d2f0cce --- /dev/null +++ b/toolkit/content/tests/chrome/test_window_intrinsic_size.xhtml @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window title="Window Open Test" + onload="runTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<script class="testbody" type="application/javascript"><![CDATA[ + SimpleTest.waitForExplicitFinish(); + + function openWindow(features = "") { + return window.browsingContext.topChromeWindow + .openDialog("window_intrinsic_size.xhtml", + "", "chrome,dialog=no,all," + features); + } + + function checkWindowSize(win, width, height, msg) { + is(win.innerWidth, width, "width should match " + msg); + is(win.innerHeight, height, "height should match " + msg); + } + + async function runTest() { + let win = openWindow(); + await SimpleTest.promiseFocus(win); + checkWindowSize(win, 300, 500, "with width attribute specified"); + + win = openWindow("width=400"); + await SimpleTest.promiseFocus(win); + checkWindowSize(win, 400, 500, "with width feature specified"); + + SimpleTest.finish(); + } +]]></script> +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</window> diff --git a/toolkit/content/tests/chrome/window_browser_drop.xhtml b/toolkit/content/tests/chrome/window_browser_drop.xhtml new file mode 100644 index 0000000000..cb23ef74de --- /dev/null +++ b/toolkit/content/tests/chrome/window_browser_drop.xhtml @@ -0,0 +1,249 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Browser Drop Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +const { ContentTask } = ChromeUtils.importESModule( + "resource://testing-common/ContentTask.sys.mjs" +); + +function dropOnRemoteBrowserAsync(browser, data, shouldExpectStateChange) { + ContentTask.setTestScope(window); // Need this so is/isnot/ok are available inside the contenttask + return ContentTask.spawn(browser, {data, shouldExpectStateChange}, async function({data, shouldExpectStateChange}) { + if (!content.document.documentElement) { + // Wait until the testing document gets loaded. + await new Promise(resolve => { + let onload = function() { + content.window.removeEventListener("load", onload); + resolve(); + }; + content.window.addEventListener("load", onload); + }); + } + + let dataTransfer = new content.DataTransfer(); + for (let i = 0; i < data.length; i++) { + let types = data[i]; + for (let j = 0; j < types.length; j++) { + dataTransfer.mozSetDataAt(types[j].type, types[j].data, i); + } + } + let event = content.document.createEvent("DragEvent"); + event.initDragEvent("drop", true, true, content, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + content.document.body.dispatchEvent(event); + + let links = []; + try { + links = Services.droppedLinkHandler.dropLinks(event, true); + } catch (ex) { + if (shouldExpectStateChange) { + ok(false, "Should not have gotten an exception from the dropped link handler, but got: " + ex); + console.error(ex); + } + } + + return links; + }); +} + +async function expectLink(browser, expectedLinks, data, testid, onbody=false) { + let lastLinks = []; + let lastLinksPromise = new Promise(resolve => { + browser.droppedLinkHandler = function(event, links) { + info(`droppedLinkHandler called, received links ${JSON.stringify(links)}`); + if (!expectedLinks.length) { + ok(false, `droppedLinkHandler called for ${JSON.stringify(links)} which we didn't expect.`); + } + lastLinks = links; + resolve(links); + }; + }); + + function dropOnBrowserSync() { + let dropEl = onbody ? browser.contentDocument.body : browser; + synthesizeDrop(dropEl, dropEl, data, null, dropEl.ownerGlobal); + } + let links; + if (browser.isRemoteBrowser) { + let remoteLinks = await dropOnRemoteBrowserAsync(browser, data, expectedLinks.length); + is(remoteLinks.length, expectedLinks.length, testid + " remote links length"); + for (let i = 0, length = remoteLinks.length; i < length; i++) { + is(remoteLinks[i].url, expectedLinks[i].url, testid + "[" + i + "] remote link"); + is(remoteLinks[i].name, expectedLinks[i].name, testid + "[" + i + "] remote name"); + } + + if (!expectedLinks.length) { + // There is no way to check if nothing happens asynchronously. + return; + } + + links = await lastLinksPromise; + } else { + dropOnBrowserSync(); + links = lastLinks; + } + + is(links.length, expectedLinks.length, testid + " links length"); + for (let i = 0, length = links.length; i < length; i++) { + is(links[i].url, expectedLinks[i].url, testid + "[" + i + "] link"); + is(links[i].name, expectedLinks[i].name, testid + "[" + i + "] name"); + } +}; + +async function dropLinksOnBrowser(browser, type) { + // Dropping single text/plain item with single link should open single + // page. + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" } ] ], + "text/plain drop on browser " + type); + + // Dropping single text/plain item with multiple links should open + // multiple pages. + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }, + { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/\nhttp://www.example.com/" } ] ], + "text/plain with 2 URLs drop on browser " + type); + + // Dropping sinlge unsupported type item should not open anything. + await expectLink(browser, + [], + [ [ { type: "text/link", + data: "http://www.mozilla.org/" } ] ], + "text/link drop on browser " + type); + + // Dropping single text/uri-list item with single link should open single + // page. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" } ], + [ [ { type: "text/uri-list", + data: "http://www.example.com/" } ] ], + "text/uri-list drop on browser " + type); + + // Dropping single text/uri-list item with multiple links should open + // multiple pages. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "http://www.example.com/" }, + { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" }], + [ [ { type: "text/uri-list", + data: "http://www.example.com/\nhttp://www.mozilla.org/" } ] ], + "text/uri-list with 2 URLs drop on browser " + type); + + // Name in text/x-moz-url should be handled. + await expectLink(browser, + [ { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.example.com/\nExample.com" } ] ], + "text/x-moz-url drop on browser " + type); + + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "Mozilla.org" }, + { url: "http://www.example.com/", + name: "Example.com" } ], + [ [ { type: "text/x-moz-url", + data: "http://www.mozilla.org/\nMozilla.org\nhttp://www.example.com/\nExample.com" } ] ], + "text/x-moz-url with 2 URLs drop on browser " + type); + + // Dropping single item with multiple types should open single page. + await expectLink(browser, + [ { url: "http://www.example.org/", + name: "Example.com" } ], + [ [ { type: "text/plain", + data: "http://www.mozilla.org/" }, + { type: "text/x-moz-url", + data: "http://www.example.org/\nExample.com" } ] ], + "text/plain and text/x-moz-url drop on browser " + type); + + // Dropping javascript or data: URLs should fail: + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "javascript:'bad'" } ] ], + "text/plain javascript url drop on browser " + type); + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "jAvascript:'also bad'" } ] ], + "text/plain mixed-case javascript url drop on browser " + type); + await expectLink(browser, + [], + [ [ { type: "text/plain", + data: "data:text/html,bad" } ] ], + "text/plain data url drop on browser " + type); + + // Dropping a chrome url should fail as we don't have a source node set, + // defaulting to a source of file:/// + await expectLink(browser, + [], + [ [ { type: "text/x-moz-url", + data: "chrome://browser/content/browser.xhtml" } ] ], + "text/x-moz-url chrome url drop on browser " + type); + + if (browser.type == "content") { + await SpecialPowers.spawn(browser, [], function() { + content.window.stopMode = true; + }); + + // stopPropagation should not prevent the browser link handling from occuring + await expectLink(browser, + [ { url: "http://www.mozilla.org/", + name: "http://www.mozilla.org/" } ], + [ [ { type: "text/uri-list", + data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with stopPropagation drop event", true); + + await SpecialPowers.spawn(browser, [], function() { + content.window.cancelMode = true; + }); + + // Canceling the event, however, should prevent the link from being handled. + await expectLink(browser, + [], + [ [ { type: "text/uri-list", data: "http://www.mozilla.org/" } ] ], + "text/x-moz-url drop on browser with cancelled drop event", true); + } +} + +function info(msg) { window.arguments[0].SimpleTest.info(msg); } +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +]]> +</script> + +<html:style> + /* FIXME: This is just preserving behavior from before bug 1656081. + * Without this there's one subtest that fails, but the browser + * elements were zero-sized before so... */ + browser { + min-width: 0; + min-height: 0; + } +</html:style> + +<browser id="chromechild" src="about:blank"/> +<browser id="contentchild" type="content" style="width: 100px; height: 100px" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> +<browser id="remote-contentchild" type="content" remote="true" style="width: 100px; height: 100px" + src="data:text/html,<html draggable='true'><body draggable='true' style='width: 100px; height: 100px;' ondragover='event.preventDefault()' ondrop='if (window.stopMode) event.stopPropagation(); if (window.cancelMode) event.preventDefault();'></body></html>"/> +</window> diff --git a/toolkit/content/tests/chrome/window_chromemargin.xhtml b/toolkit/content/tests/chrome/window_chromemargin.xhtml new file mode 100644 index 0000000000..81bcba62fe --- /dev/null +++ b/toolkit/content/tests/chrome/window_chromemargin.xhtml @@ -0,0 +1,69 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +chrome margins rock! +<script> + +// Tests parsing of the chrome margin attrib on a window. + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function doSingleTest(param) +{ + var exception = null; + try { + document.documentElement.removeAttribute("chromemargin"); + document.documentElement.setAttribute("chromemargin", param); + ok(document. + documentElement. + getAttribute("chromemargin") == param, "couldn't set/get chromemargin?"); + } catch (ex) { + exception = ex; + } + ok(!exception, "failed for param:'" + param + "'"); + return true; +} + +function runTests() +{ + var doc = document.documentElement; + + // make sure we can set and get + doc.setAttribute("chromemargin", "0,0,0,0"); + ok(doc.getAttribute("chromemargin") == "0,0,0,0", "couldn't set/get chromemargin?"); + doc.setAttribute("chromemargin", "-1,-1,-1,-1"); + ok(doc.getAttribute("chromemargin") == "-1,-1,-1,-1", "couldn't set/get chromemargin?"); + + // test remove + doc.removeAttribute("chromemargin"); + ok(doc.getAttribute("chromemargin") == "", "couldn't remove chromemargin?"); + + // we already test these really well in a c++ test in widget + doSingleTest("1,2,3,4"); + doSingleTest("-2,-2,-2,-2"); + doSingleTest("1,1,1,1"); + doSingleTest(""); + doSingleTest("12123123"); + doSingleTest("0,-1,-1,-1"); + doSingleTest("-1,0,-1,-1"); + doSingleTest("-1,-1,0,-1"); + doSingleTest("-1,-1,-1,0"); + doSingleTest("1234567890,1234567890,1234567890,1234567890"); + doSingleTest("-1,-1,-1,-1"); + + window.arguments[0].SimpleTest.finish(); + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml new file mode 100644 index 0000000000..d5c0e2753e --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_dialog.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Cursor snapping test" + width="600" height="600" + onload="onload();" + onunload="onunload();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog id="dialog" + buttons="accept,cancel"> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.arguments[0].SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.arguments[0].SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.arguments[0].canRetryTest(); +} + +function getTimeoutTime() +{ + return window.arguments[0].getTimeoutTime(); +} + +var gTimer; +var gRetry; + +function finishByTimeout() +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) { + ok(true, "cursor is NOT snapped to the disabled button (dialog)"); + } else if (button.hidden) { + ok(true, "cursor is NOT snapped to the hidden button (dialog)"); + } else if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (dialog)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove(aEvent) +{ + var button = document.getElementById("dialog").getButton("accept"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (dialog)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (dialog)"); + else + ok(true, "cursor IS snapped to the default button (dialog)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("dialog").getButton("accept"); + button.addEventListener("mousemove", onMouseMove); + + if (window.arguments[0].gDisable) { + button.disabled = true; + } + if (window.arguments[0].gHidden) { + button.hidden = true; + } + gRetry = false; + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.arguments[0].retryCurrentTest(); + } else { + window.arguments[0].runNextTest(); + } +} + +]]> +</script> + +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml new file mode 100644 index 0000000000..661e9c4e65 --- /dev/null +++ b/toolkit/content/tests/chrome/window_cursorsnap_wizard.xhtml @@ -0,0 +1,109 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<wizard title="Cursor snapping test" id="wizard" + width="600" height="600" + onload="onload();" + onunload="onunload();" + buttons="accept,cancel" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <wizardpage> + <label value="first page"/> + </wizardpage> + + <wizardpage> + <label value="second page"/> + </wizardpage> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +function ok(aCondition, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.ok(aCondition, aMessage); +} + +function is(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.is(aLeft, aRight, aMessage); +} + +function isnot(aLeft, aRight, aMessage) +{ + window.opener.wrappedJSObject.SimpleTest.isnot(aLeft, aRight, aMessage); +} + +function canRetryTest() +{ + return window.opener.wrappedJSObject.canRetryTest(); +} + +function getTimeoutTime() +{ + return window.opener.wrappedJSObject.getTimeoutTime(); +} + +var gTimer; +var gRetry = false; + +function finishByTimeout() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) { + ok(true, "cursor is NOT snapped to the disabled button (wizard)"); + } else if (button.hidden) { + ok(true, "cursor is NOT snapped to the hidden button (wizard)"); + } else if (!canRetryTest()) { + ok(false, "cursor is NOT snapped to the default button (wizard)"); + } else { + // otherwise, this may be unexpected timeout, we should retry the test. + gRetry = true; + } + finish(); +} + +function finish() +{ + window.close(); +} + +function onMouseMove() +{ + var button = document.getElementById("wizard").getButton("next"); + if (button.disabled) + ok(false, "cursor IS snapped to the disabled button (wizard)"); + else if (button.hidden) + ok(false, "cursor IS snapped to the hidden button (wizard)"); + else + ok(true, "cursor IS snapped to the default button (wizard)"); + clearTimeout(gTimer); + finish(); +} + +function onload() +{ + var button = document.getElementById("wizard").getButton("next"); + button.addEventListener("mousemove", onMouseMove); + + if (window.opener.wrappedJSObject.gDisable) { + button.disabled = true; + } + if (window.opener.wrappedJSObject.gHidden) { + button.hidden = true; + } + gTimer = setTimeout(finishByTimeout, getTimeoutTime()); +} + +function onunload() +{ + if (gRetry) { + window.opener.wrappedJSObject.retryCurrentTest(); + } else { + window.opener.wrappedJSObject.runNextTest(); + } +} + +]]> +</script> + +</wizard> diff --git a/toolkit/content/tests/chrome/window_intrinsic_size.xhtml b/toolkit/content/tests/chrome/window_intrinsic_size.xhtml new file mode 100644 index 0000000000..cae3d78594 --- /dev/null +++ b/toolkit/content/tests/chrome/window_intrinsic_size.xhtml @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="300" + style="min-height: 500px"> +</window> diff --git a/toolkit/content/tests/chrome/window_keys.xhtml b/toolkit/content/tests/chrome/window_keys.xhtml new file mode 100644 index 0000000000..77a098ef0b --- /dev/null +++ b/toolkit/content/tests/chrome/window_keys.xhtml @@ -0,0 +1,205 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Key Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script> +<![CDATA[ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +SimpleTest.waitForExplicitFinish(); + +var gExpected = null; + + +const kIsWin = AppConstants.platform == "win"; +const kIsMac = AppConstants.platform == "macosx"; + +// Only on Windows, metaKey state is ignored when there is no shortcut key handler +// which exactly matches with metaKey state. Therefore the following tests +// checks kIsWin in some cases which has metaKey. +var keysToTest = [ + ["k-v", "V", { } ], + ["", "V", { shiftKey: true } ], + ["k-v-scy", "V", { ctrlKey: true } ], + ["", "V", { altKey: true } ], + [kIsWin ? "k-v" : "", "V", { metaKey: true } ], + ["k-v-scy", "V", { shiftKey: true, ctrlKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-e-y", "E", { } ], + ["", "E", { shiftKey: true } ], + ["", "E", { ctrlKey: true } ], + ["", "E", { altKey: true } ], + [kIsWin ? "k-e-y" : "", "E", { metaKey: true } ], + ["k-d-a", "D", { altKey: true } ], + ["k-8-m", "8", { metaKey: true } ], + ["", "B", {} ], + ["k-c-scaym", "C", { metaKey: true } ], + ["k-c-scaym", "C", { shiftKey: true, ctrlKey: true, altKey: true, metaKey: true } ], + ["", "V", { shiftKey: true, ctrlKey: true, altKey: true } ], + ["k-h-l", "H", { accelKey: true } ], + [kIsWin || kIsMac ? "k-h-l" : "", "H", { accelKey: true, metaKey: true } ], +// ["k-j-s", "J", { accessKey: true } ], + ["", "T", { } ], + ["k-g-c", "G", { ctrlKey: true } ], + ["k-g-cm", "G", { ctrlKey: true, metaKey: true } ], + ["scommand", "Y", { } ], + ["", "U", { } ], + ["k-z-c", "Z", { ctrlKey: true } ], +]; + +function runTest() +{ + let nonInlineKeyFired = false; + document.getElementById("k-z-c").addEventListener("command", event => { + nonInlineKeyFired = true; + checkKey(event); + }); + + iterateKeys(true, "normal"); + + ok(nonInlineKeyFired, "non-inline command listener fired"); + + var keyset = document.getElementById("keyset"); + keyset.setAttribute("disabled", "true"); + iterateKeys(false, "disabled"); + + keyset = document.getElementById("keyset"); + keyset.removeAttribute("disabled"); + iterateKeys(true, "reenabled"); + + keyset.remove(); + iterateKeys(false, "removed"); + + document.documentElement.appendChild(keyset); + iterateKeys(true, "appended"); + + var accelText = menuitem => menuitem.getAttribute("acceltext").toLowerCase(); + + $("menubutton").open = true; + + // now check if a menu updates its accelerator text when a key attribute is changed + var menuitem1 = $("menuitem1"); + ok(accelText(menuitem1).includes("d"), "menuitem1 accelText before"); + if (kIsWin) { + ok(accelText(menuitem1).includes("alt"), "menuitem1 accelText modifier before"); + } + + menuitem1.setAttribute("key", "k-s-c"); + ok(accelText(menuitem1).includes("s"), "menuitem1 accelText after"); + if (kIsWin) { + ok(accelText(menuitem1).includes("ctrl"), "menuitem1 accelText modifier after"); + } + + menuitem1.setAttribute("acceltext", "custom"); + is(accelText(menuitem1), "custom", "menuitem1 accelText set custom"); + menuitem1.removeAttribute("acceltext"); + ok(accelText(menuitem1).includes("s"), "menuitem1 accelText remove"); + if (kIsWin) { + ok(accelText(menuitem1).includes("ctrl"), "menuitem1 accelText modifier remove"); + } + + var menuitem2 = $("menuitem2"); + is(accelText(menuitem2), "", "menuitem2 accelText before"); + menuitem2.setAttribute("key", "k-s-c"); + ok(accelText(menuitem2).includes("s"), "menuitem2 accelText before"); + if (kIsWin) { + ok(accelText(menuitem2).includes("ctrl"), "menuitem2 accelText modifier before"); + } + + menuitem2.setAttribute("key", "k-h-l"); + ok(accelText(menuitem2).includes("h"), "menuitem2 accelText after"); + if (kIsWin) { + ok(accelText(menuitem2).includes("ctrl"), "menuitem2 accelText modifier after"); + } + + menuitem2.removeAttribute("key"); + is(accelText(menuitem2), "", "menuitem2 accelText after remove"); + + $("menubutton").open = false; + + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function iterateKeys(enabled, testid) +{ + for (var k = 0; k < keysToTest.length; k++) { + gExpected = keysToTest[k]; + var expectedKey = gExpected[0]; + if (!gExpected[2].accessKey || !navigator.platform.includes("Mac")) { + synthesizeKey(gExpected[1], gExpected[2]); + ok((enabled && expectedKey) || expectedKey == "k-d-a" ? + !gExpected : gExpected, testid + " key step " + (k + 1)); + } + } +} + +function checkKey(event) +{ + // the first element of the gExpected array holds the id of the <key> element + // that was expected. If this is empty, a handler wasn't expected to be called + if (gExpected[0]) + is(event.originalTarget.id, gExpected[0], "key " + gExpected[1]); + else + is("key " + event.originalTarget.id + " was activated", "", "key " + gExpected[1]); + gExpected = null; +} + +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +SimpleTest.waitForFocus(runTest); + +]]> +</script> + +<command id="scommand" oncommand="checkKey(event)"/> +<command id="scommand-disabled" disabled="true"/> + +<keyset id="keyset"> + <key id="k-v" key="v" oncommand="checkKey(event)"/> + <key id="k-v-scy" key="v" modifiers="shift any control" oncommand="checkKey(event)"/> + <key id="k-e-y" key="e" modifiers="any" oncommand="checkKey(event)"/> + <key id="k-8-m" key="8" modifiers="meta" oncommand="checkKey(event)"/> + <key id="k-c-scaym" key="c" modifiers="shift control alt any meta" oncommand="checkKey(event)"/> + <key id="k-h-l" key="h" modifiers="accel" oncommand="checkKey(event)"/> + <key id="k-j-s" key="j" modifiers="access" oncommand="checkKey(event)"/> + <key id="k-t-y" disabled="true" key="t" oncommand="checkKey(event)"/> + <key id="k-g-c" key="g" modifiers="control" oncommand="checkKey(event)"/> + <key id="k-g-cm" key="g" modifiers="control meta" oncommand="checkKey(event)"/> + <key id="k-y" key="y" command="scommand"/> + <key id="k-u" key="u" command="scommand-disabled"/> + <key id="k-z-c" key="z" modifiers="control"/> +</keyset> + +<keyset id="keyset2"> + <key id="k-d-a" key="d" modifiers="alt" oncommand="checkKey(event)"/> + <key id="k-s-c" key="s" modifiers="control" oncommand="checkKey(event)"/> +</keyset> + +<button id="menubutton" label="Menu" type="menu"> + <menupopup> + <menuitem id="menuitem1" label="Item 1" key="k-d-a"/> + <menuitem id="menuitem2" label="Item 2"/> + </menupopup> +</button> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_largemenu.xhtml b/toolkit/content/tests/chrome/window_largemenu.xhtml new file mode 100644 index 0000000000..d84b045e78 --- /dev/null +++ b/toolkit/content/tests/chrome/window_largemenu.xhtml @@ -0,0 +1,430 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Large Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<!-- + This test checks that a large menu is displayed with arrow buttons + and is on the screen. + --> + +<script> +<![CDATA[ + +var gOverflowed = false, gUnderflowed = false; +var gContextMenuTests = false; +var gScreenY = -1; +var gTestIndex = 0; +var gTests = ["open normal", "open when bottom would overlap", "open with scrolling", + "open after scrolling", "open small again", + "menu movement", "panel movement", + "context menu enough space below", + "context menu more space above", + "context menu too big either side", + "context menu larger than screen", + "context menu flips horizontally on osx"]; +function getScreenXY(element) +{ + var screenX, screenY; + var mouseFn = function(event) { + screenX = event.screenX - 1; + screenY = event.screenY - 1; + } + + // a hacky way to get the screen position of an element without using the box object + window.addEventListener("mousedown", mouseFn); + synthesizeMouse(element, 1, 1, { }); + window.removeEventListener("mousedown", mouseFn); + + return [screenX, screenY]; +} + +function hidePopup() { + window.requestAnimationFrame( + function() { + setTimeout( + function() { + document.getElementById("popup").hidePopup(); + }, 0); + }); +} + +function runTests() +{ + [, gScreenY] = getScreenXY(document.documentElement); + nextTest(); +} + +function nextTest() +{ + gOverflowed = false; gUnderflowed = false; + + var y = screen.height; + if (gTestIndex == 1) // open with bottom overlap test: + y -= 100; + else + y /= 2; + + var popup = document.getElementById("popup"); + if (gTestIndex == 2) { + // add some more menuitems so that scrolling will be necessary + var moreItemCount = Math.round(screen.height / popup.firstChild.getBoundingClientRect().height); + for (var t = 1; t <= moreItemCount; t++) { + var menu = document.createXULElement("menuitem"); + menu.setAttribute("label", "More" + t); + popup.appendChild(menu); + } + } + else if (gTestIndex == 4) { + // remove the items added in test 2 above + while (popup.childNodes.length > 15) + popup.removeChild(popup.lastChild); + } + + window.requestAnimationFrame(function() { + setTimeout( + function() { + popup.openPopupAtScreen(100, y, false); + }, 0); + }); +} + +function popupShown() +{ + if (gTests[gTestIndex] == "menu movement") + return testPopupMovement(); + + if (gContextMenuTests) + return contextMenuPopupShown(); + + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var marginTop = parseFloat(getComputedStyle(popup).marginTop); + var marginBottom = parseFloat(getComputedStyle(popup).marginBottom); + var scrollbox = popup.scrollBox.scrollbox; + var expectedScrollPos = 0; + + info(`${gTests[gTestIndex]}: ${JSON.stringify(rect)} | ${screen.width}x${screen.height} | ${gScreenY}`); + if (gTestIndex == 0) { + // the popup should be in the center of the screen + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top - marginTop) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom - marginBottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + } + else if (gTestIndex == 1) { + // the popup was supposed to open 100 pixels from the bottom, but that + // would put it off screen so ... + if (platformIsMac()) { + // On OSX the popup is constrained so it remains within the + // bounds of the screen + ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom) + gScreenY, screen.availTop + screen.availHeight, gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + else { + // On other platforms the menu should be flipped to have its bottom + // edge 100 pixels from the bottom + ok(Math.round(rect.top - marginTop) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + is(Math.round(rect.bottom + marginBottom) + gScreenY, screen.height - 100, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow"); + } + } + else if (gTestIndex == 2) { + // the popup is too large so ensure that it is on screen + ok(Math.round(rect.top - marginTop) + gScreenY >= screen.top, gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom + marginBottom) + gScreenY <= screen.height, gTests[gTestIndex] + " bottom"); + ok(gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow") + + scrollbox.scrollTo(0, 40); + expectedScrollPos = 40; + } + else if (gTestIndex == 3) { + expectedScrollPos = 40; + if (scrollbox.scrollTop != expectedScrollPos) { + // TODO(bug 1815608): This never worked on Wayland, but regressed with flexbox emulation. + todo_is(scrollbox.scrollTop, expectedScrollPos, "menu scroll position after reopening large menu should not reset"); + expectedScrollPos = 0; + } + } + else if (gTestIndex == 4) { + // note that if the height is odd, the y-offset will have been rounded + // down when we pass the fractional value to openPopupAtScreen above. + is(Math.round(rect.top - marginTop) + gScreenY, Math.floor(screen.height / 2), + gTests[gTestIndex] + " top"); + ok(Math.round(rect.bottom) + gScreenY < screen.height, + gTests[gTestIndex] + " bottom"); + ok(!gOverflowed && gUnderflowed, gTests[gTestIndex] + " overflow"); + } + + is(scrollbox.scrollTop, expectedScrollPos, "menu scroll position " + gTests[gTestIndex]) + + return hidePopup(); +} + +function is(l, r, n) { window.arguments[0].SimpleTest.is(l,r,n); } +function ok(v, n) { window.arguments[0].SimpleTest.ok(v,n); } + +var oldx, oldy, waitSteps = 0; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.arguments[0].SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +function popupHidden() +{ + gTestIndex++; + if (gTestIndex == gTests.length) { + window.arguments[0].SimpleTest.finish(); + window.close(); + } + else if (gTests[gTestIndex] == "context menu enough space below") { + gContextMenuTests = true; + moveWindowTo(window.screenX, screen.availTop + 10, + () => synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 })); + } + else if (gTests[gTestIndex] == "menu movement") { + document.getElementById("popup").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gTests[gTestIndex] == "panel movement") { + document.getElementById("panel").openPopup( + document.getElementById("label"), "after_start", 0, 0, false, false); + } + else if (gContextMenuTests) { + contextMenuPopupHidden(); + } + else { + nextTest(); + } +} + +function contextMenuPopupShown() +{ + var popup = document.getElementById("popup"); + var rect = popup.getBoundingClientRect(); + var marginTop = parseFloat(getComputedStyle(popup).marginTop); + var marginLeft = parseFloat(getComputedStyle(popup).marginLeft); + var labelrect = document.getElementById("label").getBoundingClientRect(); + + // Click to open popup in popupHidden() occurs at (4,4) in label's coordinate space + var clickX = 4; + var clickY = 4; + + info(`${gTests[gTestIndex]}: ${JSON.stringify(rect)}`); + + var testPopupAppearedRightOfCursor = true; + switch (gTests[gTestIndex]) { + case "context menu enough space below": + is(rect.top - marginTop, labelrect.top + clickY + (platformIsMac() ? -6 : 2), gTests[gTestIndex] + " top"); + break; + case "context menu more space above": + if (platformIsMac()) { + let screenY; + [, screenY] = getScreenXY(popup); + // Macs constrain their popup menus vertically rather than flip them. + is(screenY, screen.availTop + screen.availHeight - rect.height, gTests[gTestIndex] + " top"); + } else { + is(rect.top + marginTop, labelrect.top + clickY - rect.height - 2, gTests[gTestIndex] + " top"); + } + + break; + case "context menu too big either side": + [, gScreenY] = getScreenXY(document.documentElement); + // compare against the available size as well as the total size, as some + // platforms allow the menu to overlap os chrome and others do not + var pos = (screen.availTop + screen.availHeight - rect.height) - gScreenY - marginTop; + var availPos = (screen.top + screen.height - rect.height) - gScreenY - marginTop; + ok(rect.top == pos || rect.top == availPos, + gTests[gTestIndex] + ` top (${pos}/${availPos})`); + break; + case "context menu larger than screen": + ok(rect.top == -(gScreenY - screen.availTop) + marginTop || rect.top == -(gScreenY - screen.top) + marginTop, + `${gTests[gTestIndex]} top (top = ${rect.top} screenY = ${gScreenY} screenAvailTop = ${screen.availTop} screenTop = ${screen.top})`); + break; + case "context menu flips horizontally on osx": + testPopupAppearedRightOfCursor = false; + if (platformIsMac()) { + is(Math.round(rect.right), labelrect.left + clickX - 1, gTests[gTestIndex] + " right"); + } + break; + } + + if (testPopupAppearedRightOfCursor) { + is(rect.left - marginLeft, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left"); + } + + hidePopup(); +} + +function contextMenuPopupHidden() +{ + var screenAvailBottom = screen.availTop + screen.availHeight; + + if (gTests[gTestIndex] == "context menu more space above") { + moveWindowTo(window.screenX, screenAvailBottom - 80, nextContextMenuTest, -1); + } + else if (gTests[gTestIndex] == "context menu too big either side") { + moveWindowTo(window.screenX, screenAvailBottom / 2 - 80, nextContextMenuTest, screenAvailBottom / 2 + 120); + } + else if (gTests[gTestIndex] == "context menu larger than screen") { + nextContextMenuTest(screen.availHeight + 80); + } + else if (gTests[gTestIndex] == "context menu flips horizontally on osx") { + var popup = document.getElementById("popup"); + var popupWidth = popup.getBoundingClientRect().width; + moveWindowTo(screen.availLeft + screen.availWidth - popupWidth, 100, nextContextMenuTest, -1); + } +} + +function nextContextMenuTest(desiredHeight) +{ + if (desiredHeight >= 0) { + var popup = document.getElementById("popup"); + var height = popup.getBoundingClientRect().height; + var itemheight = document.getElementById("firstitem").getBoundingClientRect().height; + while (height < desiredHeight) { + var menu = document.createXULElement("menuitem"); + menu.setAttribute("label", "Item"); + popup.appendChild(menu); + height += itemheight; + } + } + + synthesizeMouse(document.getElementById("label"), 4, 4, { type: "contextmenu", button: 2 }); +} + +function testPopupMovement() +{ + var isPanelTest = (gTests[gTestIndex] == "panel movement"); + var popup = document.getElementById(isPanelTest ? "panel" : "popup"); + + var screenX, screenY; + var rect = popup.getBoundingClientRect(); + var marginBottom = parseFloat(getComputedStyle(popup).marginBottom); + var marginLeft = parseFloat(getComputedStyle(popup).marginLeft); + var marginTop = parseFloat(getComputedStyle(popup).marginTop); + + var panelIsTop = SpecialPowers.getBoolPref("ui.panel.default_level_parent"); + var overlapOSChrome = !platformIsMac() && (!isPanelTest || panelIsTop); + popup.moveTo(1, 1); + [screenX, screenY] = getScreenXY(popup); + + var expectedx = 1, expectedy = 1; + if (!overlapOSChrome) { + if (screen.availLeft >= 1) expectedx = screen.availLeft; + if (screen.availTop >= 1) expectedy = screen.availTop; + } + is(screenX, expectedx, gTests[gTestIndex] + " (1, 1) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (1, 1) y"); + + popup.moveTo(100, 8000); + expectedy = (overlapOSChrome ? screen.height + screen.top : screen.availHeight + screen.availTop) - + Math.round(rect.height) - marginBottom; + [screenX, screenY] = getScreenXY(popup); + is(screenX, 100, gTests[gTestIndex] + " (100, 8000) x"); + is(screenY, expectedy, gTests[gTestIndex] + " (100, 8000) y"); + + popup.moveTo(6000, 100); + + expectedx = (overlapOSChrome ? screen.width + screen.left : screen.availWidth + screen.availLeft) - + Math.round(rect.width) - marginLeft; + [screenX, screenY] = getScreenXY(popup); + is(screenX, expectedx, gTests[gTestIndex] + " (6000, 100) x"); + is(screenY, 100, gTests[gTestIndex] + " (6000, 100) y"); + + is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is empty after moving"); + is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is empty after moving"); + popup.setAttribute("left", "80"); + popup.setAttribute("top", "82"); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 80, gTests[gTestIndex] + " set left and top x"); + is(screenY, 82, gTests[gTestIndex] + " set left and top y"); + popup.moveTo(95, 98); + [screenX, screenY] = getScreenXY(popup); + is(screenX, 95, gTests[gTestIndex] + " move after set left and top x"); + is(screenY, 98, gTests[gTestIndex] + " move after set left and top y"); + is(popup.getAttribute("left"), "95", gTests[gTestIndex] + " left is set after moving"); + is(popup.getAttribute("top"), "98", gTests[gTestIndex] + " top is set after moving"); + popup.removeAttribute("left"); + popup.removeAttribute("top"); + + popup.moveTo(-1 + marginLeft, -1 + marginTop); + [screenX, screenY] = getScreenXY(popup); + + expectedx = (overlapOSChrome ? screen.left : screen.availLeft) + marginLeft; + expectedy = (overlapOSChrome ? screen.top : screen.availTop) + marginTop; + + is(screenX, expectedx, gTests[gTestIndex] + " move after set left and top x to -1"); + is(screenY, expectedy, gTests[gTestIndex] + " move after set left and top y to -1"); + is(popup.getAttribute("left"), "", gTests[gTestIndex] + " left is not set after moving to -1"); + is(popup.getAttribute("top"), "", gTests[gTestIndex] + " top is not set after moving to -1"); + + popup.hidePopup(); +} + +function platformIsMac() +{ + return navigator.platform.indexOf("Mac") > -1; +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +]]> +</script> + +<button id="label" label="OK" context="popup"/> +<menupopup id="popup" onpopupshown="popupShown();" onpopuphidden="popupHidden();" + onoverflow="gOverflowed = true" onunderflow="gUnderflowed = true;"> + <menuitem id="firstitem" label="1"/> + <menuitem label="2"/> + <menuitem label="3"/> + <menuitem label="4"/> + <menuitem label="5"/> + <menuitem label="6"/> + <menuitem label="7"/> + <menuitem label="8"/> + <menuitem label="9"/> + <menuitem label="10"/> + <menuitem label="11"/> + <menuitem label="12"/> + <menuitem label="13"/> + <menuitem label="14"/> + <menuitem label="15"/> +</menupopup> + +<panel id="panel" onpopupshown="testPopupMovement();" onpopuphidden="popupHidden();" style="margin: 0; -moz-window-input-region-margin: 0;"> + <button label="OK"/> +</panel> + +</window> diff --git a/toolkit/content/tests/chrome/window_maximized_persist.xhtml b/toolkit/content/tests/chrome/window_maximized_persist.xhtml new file mode 100644 index 0000000000..f7eb695f0f --- /dev/null +++ b/toolkit/content/tests/chrome/window_maximized_persist.xhtml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + height="300" + width="300" + sizemode="normal" + id="window" + persist="height width sizemode"> +<script type="application/javascript"><![CDATA[ + window.addEventListener("sizemodechange", evt => { + window.arguments[0].postMessage("sizemodechange", "*"); + }); +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml b/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml new file mode 100644 index 0000000000..83fede7fae --- /dev/null +++ b/toolkit/content/tests/chrome/window_maximized_persist_with_no_titlebar.xhtml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + height="300" + width="300" + sizemode="normal" + chromemargin="0,2,2,2" + id="window" + persist="height width sizemode"> +<script type="application/javascript"><![CDATA[ + window.addEventListener("sizemodechange", evt => { + window.arguments[0].postMessage("sizemodechange", "*"); + }); +]]></script> +</window> diff --git a/toolkit/content/tests/chrome/window_navigate_persist.html b/toolkit/content/tests/chrome/window_navigate_persist.html new file mode 100644 index 0000000000..8b45212bed --- /dev/null +++ b/toolkit/content/tests/chrome/window_navigate_persist.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html dir="" + id="persist-window" + width="300" height="300" + persist="screenX screenY width height sizemode"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + </head> + <body> + </body> +</html> diff --git a/toolkit/content/tests/chrome/window_panel.xhtml b/toolkit/content/tests/chrome/window_panel.xhtml new file mode 100644 index 0000000000..e13d15b390 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel.xhtml @@ -0,0 +1,294 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for panels + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<html:style> +<![CDATA[ + panel, panel::part(content) { + border: 0; + margin: 0; + padding: 0; + } +]]> +</html:style> + +<tree id="tree" seltype="single" width="100" height="100"> + <treecols> + <treecol flex="1"/> + <treecol flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + </treechildren> +</tree> + + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var currentTest = null; + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +function test_panels() +{ + checkTreeCoords(); + + addEventListener("popupshowing", popupShowing, false); + addEventListener("popupshown", popupShown, false); + addEventListener("popuphidden", nextTest, false); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.arguments[0].SimpleTest.finish(); + return; + } + + currentTest = tests.shift(); + var panel = createPanel(currentTest.attrs); + currentTest.test(panel); +} + +function popupShowing(event) +{ + var rect = event.target.getOuterScreenRect(); + ok(!rect.left && !rect.top && !rect.width && !rect.height, + currentTest.testname + " empty rectangle during popupshowing"); +} + +var waitSteps = 0; +function popupShown(event) +{ + var panel = event.target; + + if (waitSteps > 0 && navigator.platform.includes("Linux") && + panel.screenY == 210) { + waitSteps--; + setTimeout(popupShown, 10, event); + return; + } + + currentTest.result(currentTest.testname + " ", panel); + panel.hidePopup(); +} + +function createPanel(attrs) +{ + var panel = document.createXULElement("panel"); + for (var a in attrs) { + panel.setAttribute(a, attrs[a]); + } + + var button = document.createXULElement("button"); + panel.appendChild(button); + button.label = "OK"; + button.setAttribute("style", "appearance: none; border: 0; margin: 0; width: 120px; height: 40px;"); + panel.setAttribute("style", "appearance: none; border: 0; margin: 0;"); + return document.documentElement.appendChild(panel); +} + +function checkTreeCoords() +{ + var tree = $("tree"); + var treechildren = $("treechildren"); + tree.currentIndex = 0; + tree.scrollToRow(0); + synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { }); + is(tree.currentIndex, 1, "tree selection"); + + tree.scrollToRow(2); + synthesizeMouse(treechildren, 10, tree.rowHeight + 2, { }); + is(tree.currentIndex, 3, "tree selection after scroll"); +} + +var tests = [ + { + testname: "normal panel", + attrs: { }, + test(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // only noautohide panels support titlebars, so one shouldn't be shown here + testname: "autohide panel with titlebar", + attrs: { titlebar: "normal" }, + test(panel) { + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, 200 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, 210 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + testname: "noautohide panel with titlebar", + attrs: { noautohide: true, titlebar: "normal" }, + test(panel) { + waitSteps = 25; + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, 0, this.testname + " screen left before open"); + is(screenRect.top, 0, this.testname + " screen top before open"); + is(screenRect.width, 0, this.testname + " screen width before open"); + is(screenRect.height, 0, this.testname + " screen height before open"); + + panel.openPopupAtScreen(200, 210); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + ok(panelrect.left >= 200 - window.mozInnerScreenX, testname + "left"); + if (!navigator.platform.includes("Linux")) { + ok(panelrect.top >= 210 - window.mozInnerScreenY, testname + "top greater " + panelrect.top + " " + window.mozInnerScreenY); + } + ok(panelrect.top <= 210 - window.mozInnerScreenY + 36, testname + "top less"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + if (!navigator.platform.includes("Linux")) { + is(screenRect.left, 200, testname + " screen left"); + is(screenRect.top, 210, testname + " screen top"); + } + ok(screenRect.width >= 120 && screenRect.width <= 140, testname + " screen width"); + ok(screenRect.height >= 40 && screenRect.height <= 118, testname + " screen height"); + + var gotMouseEvent = false; + function mouseMoved(event) + { + is(event.clientY, panelrect.top + 10, + "popup clientY"); + is(event.screenY, panel.screenY + 10, + "popup screenY"); + is(event.originalTarget, panel.firstChild, "popup target"); + gotMouseEvent = true; + } + + panel.addEventListener("mousemove", mouseMoved, true); + synthesizeMouse(panel, 10, 10, { type: "mousemove" }); + ok(gotMouseEvent, "mouse event on panel"); + panel.removeEventListener("mousemove", mouseMoved, true); + + var tree = $("tree"); + tree.currentIndex = 0; + panel.appendChild(tree); + checkTreeCoords(); + } + }, + { + // The panel should be allowed to appear and remain offscreen + testname: "normal panel with flip='none' off-screen", + attrs: { "flip": "none" }, + test(panel) { + panel.openPopup(document.documentElement, "", -100 - window.mozInnerScreenX, -100 - window.mozInnerScreenY, false, false, null); + }, + result(testname, panel) { + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -100 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, -100 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -100, testname + " screen left"); + is(screenRect.top, -100, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, + { + // The panel should be allowed to remain offscreen after moving and it should follow the anchor + testname: "normal panel with flip='none' moved off-screen", + attrs: { "flip": "none" }, + test(panel) { + panel.openPopup(document.documentElement, "", -100 - window.mozInnerScreenX, -100 - window.mozInnerScreenY, false, false, null); + window.moveBy(-50, -50); + }, + result(testname, panel) { + if (navigator.platform.includes("Linux")) { + // The window position doesn't get updated immediately on Linux. + return; + } + var panelrect = panel.getBoundingClientRect(); + is(panelrect.left, -150 - window.mozInnerScreenX, testname + "left"); + is(panelrect.top, -150 - window.mozInnerScreenY, testname + "top"); + is(panelrect.width, 120, testname + "width"); + is(panelrect.height, 40, testname + "height"); + + var screenRect = panel.getOuterScreenRect(); + is(screenRect.left, -150, testname + " screen left"); + is(screenRect.top, -150, testname + " screen top"); + is(screenRect.width, 120, testname + " screen width"); + is(screenRect.height, 40, testname + " screen height"); + } + }, +]; + +window.arguments[0].SimpleTest.waitForFocus(test_panels, window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml b/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml new file mode 100644 index 0000000000..735ecf26b6 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel_anchoradjust.xhtml @@ -0,0 +1,193 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<deck id="deck"> + <hbox id="container"> + <button id="anchor" label="Anchor"/> + </hbox> + <button id="anchor3" label="Anchor3"/> +</deck> + +<hbox id="container2"> + <button id="anchor2" label="Anchor2"/> +</hbox> + +<button id="anchor4" label="Anchor4"/> + +<panel id="panel" type="arrow"> + <button label="OK"/> +</panel> + +<menupopup id="menupopup"> + <menuitem label="One"/> + <menuitem id="menuanchor" label="Two"/> + <menuitem label="Three"/> +</menupopup> + +<script><![CDATA[ + + +SimpleTest.waitForExplicitFinish(); + +function next() +{ + return new Promise(r => { + requestAnimationFrame(() => requestAnimationFrame(r)); + }) +} + +function waitForPanel(panel, event) +{ + return new Promise(resolve => { + panel.addEventListener(event, () => { resolve(); }, { once: true }); + }); +} + +function isWithinHalfPixel(a, b, message) +{ + ok(Math.abs(a - b) <= 0.5, `${message}: ${a}, ${b}`); +} + +function getPanelPos(panel) { + let {left, top, bottom, right} = panel.getBoundingClientRect(); + left -= parseFloat(getComputedStyle(panel).marginLeft); + top -= parseFloat(getComputedStyle(panel).marginTop); + bottom += parseFloat(getComputedStyle(panel).marginBottom); + right += parseFloat(getComputedStyle(panel).marginRight); + return {left, top, bottom, right}; +} + +async function runTests() { + let panel = document.getElementById("panel"); + let anchor = document.getElementById("anchor"); + + let popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor, "after_start"); + info("popupshown"); + await popupshown; + + let anchorrect = anchor.getBoundingClientRect(); + let panelpos = getPanelPos(panel); + let xarrowdiff = panelpos.left - anchorrect.left; + + // When the anchor is moved in some manner, the panel should be adjusted + let popuppositioned = waitForPanel(panel, "popuppositioned"); + document.getElementById("anchor").style.marginLeft = "50px" + info("before popuppositioned"); + await popuppositioned; + info("after popuppositioned"); + + anchorrect = anchor.getBoundingClientRect(); + panelpos = getPanelPos(panel); + isWithinHalfPixel(anchorrect.left, panelpos.left - xarrowdiff, "anchor moved x"); + isWithinHalfPixel(anchorrect.bottom, panelpos.top, "anchor moved y"); + + // moveToAnchor is used to change the anchor + let anchor2 = document.getElementById("anchor2"); + popuppositioned = waitForPanel(panel, "popuppositioned"); + panel.moveToAnchor(anchor2, "after_end"); + info("before popuppositioned 2"); + await popuppositioned; + info("after popuppositioned 2"); + + let anchor2rect = anchor2.getBoundingClientRect(); + panelpos = getPanelPos(panel); + isWithinHalfPixel(anchor2rect.right, panelpos.right + xarrowdiff, "new anchor x"); + isWithinHalfPixel(anchor2rect.bottom, panelpos.top, "new anchor y"); + + // moveToAnchor is used to change the anchor with an x and y offset + popuppositioned = waitForPanel(panel, "popuppositioned"); + panel.moveToAnchor(anchor2, "after_end", 7, 9); + await popuppositioned; + + anchor2rect = anchor2.getBoundingClientRect(); + panelpos = getPanelPos(panel); + isWithinHalfPixel(anchor2rect.right + 7, panelpos.right + xarrowdiff, "new anchor with offset x"); + isWithinHalfPixel(anchor2rect.bottom + 9, panelpos.top, "new anchor with offset y"); + + // When the container of the anchor is collapsed, the panel should be hidden. + let popuphidden = waitForPanel(panel, "popuphidden"); + anchor2.parentNode.collapsed = true; + await popuphidden; + + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor, "after_start"); + await popupshown; + + // When the deck containing the anchor changes to a different page, the panel should be hidden. + popuphidden = waitForPanel(panel, "popuphidden"); + document.getElementById("deck").selectedIndex = 1; + await popuphidden; + + let anchor3 = document.getElementById("anchor3"); + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor3, "after_start"); + await popupshown; + + // When the anchor is hidden; the panel should be hidden. + popuphidden = waitForPanel(panel, "popuphidden"); + anchor3.parentNode.hidden = true; + await popuphidden; + + // When the panel is anchored to an element in a popup, the panel should + // also be hidden when that popup is hidden. + let menupopup = document.getElementById("menupopup"); + popupshown = waitForPanel(menupopup, "popupshown"); + menupopup.openPopupAtScreen(200, 200); + await popupshown; + + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(document.getElementById("menuanchor"), "after_start"); + await popupshown; + + popuphidden = waitForPanel(panel, "popuphidden"); + let menupopuphidden = waitForPanel(menupopup, "popuphidden"); + menupopup.hidePopup(); + await popuphidden; + await menupopuphidden; + + // The panel should no longer follow anchors. + panel.setAttribute("followanchor", "false"); + + let anchor4 = document.getElementById("anchor4"); + popupshown = waitForPanel(panel, "popupshown"); + panel.openPopup(anchor4, "after_start"); + await popupshown; + + let anchor4rect = anchor4.getBoundingClientRect(); + + anchor4.style.marginLeft = "50px" + await next(); + + panelpos = getPanelPos(panel); + isWithinHalfPixel(anchor4rect.left, panelpos.left - xarrowdiff, "no follow anchor x"); + isWithinHalfPixel(anchor4rect.bottom, panelpos.top, "no follow anchor y"); + + popuphidden = waitForPanel(panel, "popuphidden"); + panel.hidePopup(); + await popuphidden; + + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/chrome/window_panel_focus.xhtml b/toolkit/content/tests/chrome/window_panel_focus.xhtml new file mode 100644 index 0000000000..962e43db58 --- /dev/null +++ b/toolkit/content/tests/chrome/window_panel_focus.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Panel Focus Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<checkbox id="b1" label="Item 1"/> + +<!-- Focus should be in this order: 2 6 3 8 1 4 5 7 9 --> +<panel id="panel" norestorefocus="true" onpopupshown="panelShown()" onpopuphidden="panelHidden()"> + <button id="t1" label="Button One"/> + <button id="t2" tabindex="1" label="Button Two" onblur="gButtonBlur++;"/> + <button id="t3" tabindex="2" label="Button Three"/> + <button id="t4" tabindex="0" label="Button Four"/> + <button id="t5" label="Button Five"/> + <button id="t6" tabindex="1" label="Button Six"/> + <button id="t7" label="Button Seven"/> + <button id="t8" tabindex="4" label="Button Eight"/> + <button id="t9" label="Button Nine"/> +</panel> + +<panel id="noautofocusPanel" noautofocus="true" + onpopupshown="noautofocusPanelShown()" onpopuphidden="noautofocusPanelHidden()"> + <html:input id="tb3"/> +</panel> + +<checkbox id="b2" label="Item 2" popup="panel" onblur="gButtonBlur++;"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +var gButtonBlur = 0; + +function showPanel() +{ + // click on the document so that the window has focus + synthesizeMouse(document.documentElement, 1, 1, { }); + + // focus the button + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b1"), "focus", "button focus"); + // tabbing again should skip the popup + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b2"), "focus", "popup skipped in focus navigation"); + + $("panel").openPopup(null, "", 10, 10, false, false); +} + +function panelShown() +{ + // the focus on the button should have been removed when the popup was opened + is(gButtonBlur, 1, "focus removed when popup opened"); + + // press tab numerous times to cycle through the buttons. The t2 button will + // be blurred twice, so gButtonBlur will be 3 afterwards. + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t2"), "focus", "tabindex 1"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t6"), "focus", "tabindex 2"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t3"), "focus", "tabindex 3"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t8"), "focus", "tabindex 4"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t1"), "focus", "tabindex 5"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t4"), "focus", "tabindex 6"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t5"), "focus", "tabindex 7"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t7"), "focus", "tabindex 8"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t9"), "focus", "tabindex 9"); + synthesizeKeyExpectEvent("KEY_Tab", {}, $("t2"), "focus", "tabindex 10"); + + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t9"), "focus", "back tabindex 1"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t7"), "focus", "back tabindex 2"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t5"), "focus", "back tabindex 3"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t4"), "focus", "back tabindex 4"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t1"), "focus", "back tabindex 5"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t8"), "focus", "back tabindex 6"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t3"), "focus", "back tabindex 7"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t6"), "focus", "back tabindex 8"); + synthesizeKeyExpectEvent("KEY_Tab", {shiftKey: true}, $("t2"), "focus", "back tabindex 9"); + + is(gButtonBlur, 3, "blur events fired within popup"); + + synthesizeKey("KEY_Escape"); +} + +function ok(condition, message) { + window.arguments[0].SimpleTest.ok(condition, message); +} + +function is(left, right, message) { + window.arguments[0].SimpleTest.is(left, right, message); +} + +function panelHidden() +{ + // closing the popup should have blurred the focused element + is(gButtonBlur, 4, "focus removed when popup closed"); + + // now that the panel is hidden, pressing tab should focus the elements in + // the main window again + synthesizeKeyExpectEvent("KEY_Tab", {}, $("b1"), "focus", "focus after popup closed"); + + $("noautofocusPanel").openPopup(null, "", 10, 10, false, false); +} + +function noautofocusPanelShown() +{ + // with noautofocus="true", the focus should not be removed when the panel is + // opened, so key events should still be fired at the checkbox. + synthesizeKeyExpectEvent(" ", {}, $("b1"), "command", "noautofocus"); + $("noautofocusPanel").hidePopup(); +} + +function noautofocusPanelHidden() +{ + window.close(); + window.arguments[0].SimpleTest.finish(); +} + +window.arguments[0].SimpleTest.waitForFocus(showPanel, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchor.xhtml b/toolkit/content/tests/chrome/window_popup_anchor.xhtml new file mode 100644 index 0000000000..4f8e88035d --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchor.xhtml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script> +function runTests() +{ + frames[0].openPopup(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); +</script> + +<spacer style="height: 13px"/> +<button id="outerbutton" label="Button One" style="margin-left: 6px; -moz-appearance: none;"/> +<hbox> + <spacer style="width: 20px"/> + <deck> + <vbox> + <iframe id="frame" style="margin-left: 60px; margin-top: 10px; border-left: 17px solid red; padding-left: 0 !important; padding-top: 3px; width: 250px; height: 80px" + src="frame_popup_anchor.xhtml"/> + </vbox> + </deck> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml new file mode 100644 index 0000000000..524a95b643 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_anchoratrect.xhtml @@ -0,0 +1,130 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onpopupshown="popupshown(event.target)" onpopuphidden="nextTest()"> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<label value="Popup Test"/> + +<menupopup id="popup"> + <menuitem label="One"/> + <menuitem label="Two"/> +</menupopup> + +<panel id="panel" noautohide="true" height="20"> + <label value="OK"/> +</panel> + +<script> +<![CDATA[ + +let menupopup; +function margins(popup) { + let ret = {}; + let cs = getComputedStyle(popup); + for (let side of ["top", "right", "bottom", "left"]) { + ret[side] = parseFloat(cs.getPropertyValue("margin-" + side)); + } + return ret; +} + +let tests = [ + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + let margin = margins(popup); + is(rect.left - margin.left, 150, "popup at screen position x"); + is(rect.top - margin.top, 290, "popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("after_start", 150, 350, 30, 9000), + verify: popup => { + let rect = popup.getOuterScreenRect(); + let margin = margins(popup); + is(rect.left - margin.left, 150, "flipped popup at screen position x"); + is(rect.bottom + margin.bottom, 350, "flipped popup at screen position y"); + } + }, + { + test: () => menupopup.openPopupAtScreenRect("end_before", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + let margin = margins(popup); + is(rect.left - margin.left, 180, "popup at end_before screen position x"); + is(rect.top - margin.top, 250, "popup at end_before screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("after_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + let margin = margins(popup); + is(rect.left - margin.left, 150, "panel at screen position x"); + is(rect.top - margin.top, 290, "panel at screen position y"); + } + }, + { + test: () => $("panel").openPopupAtScreenRect("before_start", 150, 250, 30, 40), + verify: popup => { + let rect = popup.getOuterScreenRect(); + let margin = margins(popup); + is(rect.left - margin.left, 150, "panel at before_start screen position x"); + is(rect.bottom + margin.bottom, 250, "panel at before_start screen position y"); + } + }, +]; + +function runTest(id) +{ + menupopup = $("popup"); + nextTest(); +} + +function nextTest() +{ + if (!tests.length) { + window.close(); + window.arguments[0].SimpleTest.finish(); + return; + } + + tests[0].test(); +} + +function popupshown(popup) +{ + tests[0].verify(popup); + tests.shift(); + popup.hidePopup(); +} + +function is(left, right, message) +{ + window.arguments[0].SimpleTest.is(left, right, message); +} + +function ok(value, message) +{ + window.arguments[0].SimpleTest.ok(value, message); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_attribute.xhtml b/toolkit/content/tests/chrome/window_popup_attribute.xhtml new file mode 100644 index 0000000000..00c8e5a721 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_attribute.xhtml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Attribute Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script src="popup_shared.js"></script> + <script src="popup_trigger.js"></script> + +<html:style> + menupopup { + margin: 0; + --panel-padding: 0; + -moz-window-input-region-margin: 0; + } +</html:style> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 340px;"> + <label id="trigger" popup="thepopup" value="Popup" height="60"/> +</hbox> + +<menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> +</menupopup> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_button.xhtml b/toolkit/content/tests/chrome/window_popup_button.xhtml new file mode 100644 index 0000000000..f2456ca5a9 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_button.xhtml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> +<script src="popup_shared.js"></script> +<script src="popup_trigger.js"></script> + +<html:style> + menupopup { + margin: 0; + --panel-padding: 0; + -moz-window-input-region-margin: 0; + } +</html:style> + +<script> +window.opener.SimpleTest.waitForFocus(runTests, window); +</script> + +<hbox style="margin-left: 200px; margin-top: 340px;"> + <button id="trigger" type="menu" label="Popup" width="100" height="50"> + <menupopup id="thepopup"> + <menuitem id="item1" label="First"/> + <menuitem id="item2" label="Main Item"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem id="item3" label="Third"/> + <menuitem id="one" label="One"/> + <menuitem id="fancier" label="Fancier Menu"/> + <menu id="submenu" label="Only Menu"> + <menupopup id="submenupopup"> + <menuitem id="submenuitem" label="Test Submenu"/> + </menupopup> + </menu> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="secondlast" label="Second Last Menu" accesskey="T"/> + <menuitem id="last" label="One Other Menu"/> + </menupopup> + </button> +</hbox> + +</window> diff --git a/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml new file mode 100644 index 0000000000..43dffc2a76 --- /dev/null +++ b/toolkit/content/tests/chrome/window_popup_preventdefault_chrome.xhtml @@ -0,0 +1,126 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Popup Prevent Default Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<!-- + This tests checks that preventDefault can be called on a popupshowing + event or popuphiding event to prevent the default behaviour. + --> + +<script> + +var gBlockShowing = true; +var gBlockHiding = true; +var gShownNotAllowed = true; +var gHiddenNotAllowed = true; + +var fm = Services.focus; + +var is = function(l, r, v) { window.arguments[0].SimpleTest.is(l, r, v); } +var isnot = function(l, r, v) { window.arguments[0].SimpleTest.isnot(l, r, v); } + +const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +async function runTest() +{ + var menu = document.getElementById("menu"); + + is(fm.activeWindow, window, "active window at start"); + is(fm.focusedWindow, window, "focused window at start"); + + is(window.windowState, window.STATE_NORMAL, "window is normal"); + // the minimizing test sometimes fails on Linux so don't test it there + if (navigator.platform.indexOf("Lin") == 0) { + menu.open = true; + return; + } + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + window.minimize(); + await promiseSizeModeChange; + is(window.windowState, window.STATE_MINIMIZED, "window is minimized"); + + isnot(fm.activeWindow, window, "active window after minimize"); + isnot(fm.focusedWindow, window, "focused window after minimize"); + + menu.open = true; + + setTimeout(runTestAfterMinimize, 0); +} + +async function runTestAfterMinimize() +{ + var menu = document.getElementById("menu"); + is(menu.firstChild.state, "closed", "popup not opened when window minimized"); + + let promiseSizeModeChange = BrowserTestUtils.waitForEvent( + window, + "sizemodechange" + ); + window.restore(); + await promiseSizeModeChange; + is(window.windowState, window.STATE_NORMAL, "window is restored"); + + is(fm.activeWindow, window, "active window after restore"); + is(fm.focusedWindow, window, "focused window after restore"); + + menu.open = true; +} + +function popupShowing(event) +{ + if (gBlockShowing) { + event.preventDefault(); + gBlockShowing = false; + setTimeout(function() { + gShownNotAllowed = false; + document.getElementById("menu").open = true; + }, 3000, true); + } +} + +function popupShown() +{ + window.arguments[0].SimpleTest.ok(!gShownNotAllowed, "popupshowing preventDefault"); + document.getElementById("menu").open = false; +} + +function popupHiding(event) +{ + if (gBlockHiding) { + event.preventDefault(); + gBlockHiding = false; + setTimeout(function() { + gHiddenNotAllowed = false; + document.getElementById("menu").open = false; + }, 3000, true); + } +} + +function popupHidden() +{ + window.arguments[0].SimpleTest.ok(!gHiddenNotAllowed, "popuphiding preventDefault"); + window.arguments[0].SimpleTest.finish(); + window.close(); +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); +</script> + +<button id="menu" type="menu" label="Menu"> + <menupopup onpopupshowing="popupShowing(event);" + onpopupshown="popupShown();" + onpopuphiding="popupHiding(event);" + onpopuphidden="popupHidden();"> + <menuitem label="Item"/> + </menupopup> +</button> + + +</window> diff --git a/toolkit/content/tests/chrome/window_preferences.xhtml b/toolkit/content/tests/chrome/window_preferences.xhtml new file mode 100644 index 0000000000..273bd7060e --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + class="prefwindow" + title="preferences window" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="window_preferences_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + setTimeout(() => { + // run test + aArgs[0](this); + // close dialog + let dialog = document.getElementById("window_preferences_dialog"); + dialog[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + }); + } + + Preferences.addAll([ + // one of each type known to Preference.valueFromPreferences + { id: "tests.static_preference_int", type: "int" }, + { id: "tests.static_preference_bool", type: "bool" }, + { id: "tests.static_preference_string", type: "string" }, + { id: "tests.static_preference_wstring", type: "wstring" }, + { id: "tests.static_preference_unichar", type: "unichar" }, + { id: "tests.static_preference_file", type: "file" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <html:input id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <html:input id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <html:input id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <html:input id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <html:input id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences2.xhtml b/toolkit/content/tests/chrome/window_preferences2.xhtml new file mode 100644 index 0000000000..af51f5df34 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences2.xhtml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="pw 2" + windowtype="test:preferences2" + onload="RunTest(window.arguments)"> +<dialog id="window_preferences2_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + function RunTest(aArgs) + { + // open child + openDialog("window_preferences3.xhtml", "", "modal,centerscreen,resizable=no", {test: aArgs[0], accept: aArgs[1]}); + // close dialog + let dialog = document.getElementById("window_preferences2_dialog"); + dialog[aArgs[1] ? "acceptDialog" : "cancelDialog"](); + } + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences3.xhtml b/toolkit/content/tests/chrome/window_preferences3.xhtml new file mode 100644 index 0000000000..719c66a737 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences3.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + class="prefwindow" + title="pw 3" + windowtype="test:preferences3" + onload="RunTest(window.arguments)" + type="child"> +<dialog id="window_preferences3_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + // run test + aArgs[0].test(this); + // close dialog + let dialog = document.getElementById("window_preferences3_dialog"); + dialog[aArgs[0].accept ? "acceptDialog" : "cancelDialog"](); + } + + Preferences.addAll([ + // one of each type known to Preference.valueFromPreferences + { id: "tests.static_preference_int", type: "int" }, + { id: "tests.static_preference_bool", type: "bool" }, + { id: "tests.static_preference_string", type: "string" }, + { id: "tests.static_preference_wstring", type: "wstring" }, + { id: "tests.static_preference_unichar", type: "unichar" }, + { id: "tests.static_preference_file", type: "file" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <!-- one element for each preference type above --> + <hbox> + <label flex="1" value="int"/> + <html:input id="static_element_int" preference="tests.static_preference_int"/> + </hbox> + <hbox> + <label flex="1" value="bool"/> + <checkbox id="static_element_bool" preference="tests.static_preference_bool"/> + </hbox> + <hbox> + <label flex="1" value="string"/> + <html:input id="static_element_string" preference="tests.static_preference_string"/> + </hbox> + <hbox> + <label flex="1" value="wstring"/> + <html:input id="static_element_wstring" preference="tests.static_preference_wstring"/> + </hbox> + <hbox> + <label flex="1" value="unichar"/> + <html:input id="static_element_unichar" preference="tests.static_preference_unichar"/> + </hbox> + <hbox> + <label flex="1" value="file"/> + <html:input id="static_element_file" preference="tests.static_preference_file"/> + </hbox> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml new file mode 100644 index 0000000000..8d2e54d54d --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_beforeaccept.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window with beforeaccept +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="pw beforeaccept" + width="300" height="300" + windowtype="test:preferences" + type="child" + onload="onDialogLoad();"> +<dialog id="beforeaccept_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function onDialogLoad() { + document.addEventListener("beforeaccept", beforeAccept); + var pref = Preferences.get("tests.beforeaccept.dialogShown"); + pref.value = true; + + // call the onload handler we were passed + window.arguments[0](); + } + + function beforeAccept(event) { + var beforeAcceptPref = window.Preferences.get("tests.beforeaccept.called"); + var oldValue = beforeAcceptPref.value; + beforeAcceptPref.value = true; + + if (!oldValue) { + event.preventDefault(); + } + } + + Preferences.addAll([ + { id: "tests.beforeaccept.called", type: "bool" }, + { id: "tests.beforeaccept.dialogShown", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + </vbox> + <label>Test Prefpane</label> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml b/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml new file mode 100644 index 0000000000..52942d9da1 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_commandretarget.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window. This particular test ensures that + a checkbox with a command attribute properly updates even though the command + event gets retargeted. +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="pw commandretarget" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="commandretarget_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + aArgs[0](this); + let dialog = document.getElementById("commandretarget_dialog"); + dialog.cancelDialog(); + } + + Preferences.addAll([ + { id: "tests.static_preference_bool", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <commandset> + <command id="cmd_test" preference="tests.static_preference_bool"/> + </commandset> + + <checkbox id="checkbox" label="Enable Option" preference="tests.static_preference_bool" command="cmd_test"/> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_disabled.xhtml b/toolkit/content/tests/chrome/window_preferences_disabled.xhtml new file mode 100644 index 0000000000..4bb17bbeac --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_disabled.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- + XUL Widget Test for preferences window. This particular test ensures that + when a preference is disabled, the checkbox disabled and when a preference + is locked, the checkbox can't be enabled. +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="pw disabled" + windowtype="test:preferences" + onload="RunTest(window.arguments)"> +<dialog id="disabled_dialog" + buttons="accept,cancel"> + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + function RunTest(aArgs) + { + setTimeout(() => { + aArgs[0](this); + let dialog = document.getElementById("disabled_dialog"); + dialog.cancelDialog(); + }); + } + + Preferences.addAll([ + { id: "tests.disabled_preference_bool", type: "bool" }, + { id: "tests.locked_preference_bool", type: "bool" }, + ]); + ]]> + </script> + + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + <checkbox id="disabled_checkbox" label="Disabled" preference="tests.disabled_preference_bool"/> + <checkbox id="locked_checkbox" label="Locked" preference="tests.locked_preference_bool"/> + </vbox> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml new file mode 100644 index 0000000000..6ff07e1620 --- /dev/null +++ b/toolkit/content/tests/chrome/window_preferences_onsyncfrompreference.xhtml @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!-- + XUL Widget Test for preferences window with onsyncfrompreference + This test ensures that onsyncfrompreference handlers are called after all the + values of the corresponding preference element have been set correctly +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + class="prefwindow" + title="pw onsyncfrompreference" + width="300" height="300" + windowtype="test:preferences" + onload="onLoad()"> +<dialog> + + <script type="application/javascript" src="chrome://global/content/preferencesBindings.js"/> + <script type="application/javascript"> + <![CDATA[ + /* import-globals-from ../../preferencesBindings.js */ + Preferences.addAll([ + { id: "tests.onsyncfrompreference.pref1", type: "int" }, + { id: "tests.onsyncfrompreference.pref2", type: "int" }, + { id: "tests.onsyncfrompreference.pref3", type: "int" }, + ]); + + function onLoad() { + Preferences.addSyncFromPrefListener(document.getElementById("check1"), + () => window.arguments[0]()); + Preferences.addSyncFromPrefListener(document.getElementById("check2"), + () => window.arguments[0]()); + Preferences.addSyncFromPrefListener(document.getElementById("check3"), + () => window.arguments[0]()); + Preferences.addSyncToPrefListener(document.getElementById("check1"), + () => 1); + Preferences.addSyncToPrefListener(document.getElementById("check2"), + () => 1); + Preferences.addSyncToPrefListener(document.getElementById("check3"), + () => 1); + } + ]]> + </script> + <vbox id="sample_pane" class="prefpane" label="Sample Prefpane"> + </vbox> + <label>Test Prefpane</label> + <checkbox id="check1" label="Label1" + preference="tests.onsyncfrompreference.pref1"/> + <checkbox id="check2" label="Label2" + preference="tests.onsyncfrompreference.pref2"/> + <checkbox id="check3" label="Label3" + preference="tests.onsyncfrompreference.pref3"/> +</dialog> +</window> diff --git a/toolkit/content/tests/chrome/window_screenPosSize.xhtml b/toolkit/content/tests/chrome/window_screenPosSize.xhtml new file mode 100644 index 0000000000..accc10d8f1 --- /dev/null +++ b/toolkit/content/tests/chrome/window_screenPosSize.xhtml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Window Open Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + screenX="80" + screenY="80" + height="300" + width="300" + persist="screenX screenY height width"> + +<body xmlns="http://www.w3.org/1999/xhtml"> + +</body> + +</window> diff --git a/toolkit/content/tests/chrome/window_showcaret.xhtml b/toolkit/content/tests/chrome/window_showcaret.xhtml new file mode 100644 index 0000000000..4c508d52a0 --- /dev/null +++ b/toolkit/content/tests/chrome/window_showcaret.xhtml @@ -0,0 +1,11 @@ +<?xml version='1.0'?> + +<?xml-stylesheet href='chrome://global/skin' type='text/css'?> + +<window xmlns='http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' + xmlns:html="http://www.w3.org/1999/xhtml"> + +<hbox style='-moz-user-focus: normal;' width='20' height='20'/> +<html:input/> + +</window> diff --git a/toolkit/content/tests/chrome/window_subframe_origin.xhtml b/toolkit/content/tests/chrome/window_subframe_origin.xhtml new file mode 100644 index 0000000000..65d0a043fd --- /dev/null +++ b/toolkit/content/tests/chrome/window_subframe_origin.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window id="window" title="Subframe Origin Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<iframe + style="margin-left:20px; margin-top:20px; min-height:300px; max-width:300px; max-height:300px; border:solid 1px black;" + src="frame_subframe_origin_subframe1.xhtml"></iframe> +<caption id="parentcap" label=""/> + +<script> + +// Fire a mouse move event aimed at this window, and check to be +// sure the client coords translate from widget to the dom correctly. + +function runTests() +{ + synthesizeMouse(document.getElementById("window"), 1, 2, { type: "mousemove" }); +} + +window.arguments[0].SimpleTest.waitForFocus(runTests, window); + +function mouseMove(e) { + var el = document.getElementById("parentcap"); + el.label = "client: (" + e.clientX + "," + e.clientY + ")"; + window.arguments[0].SimpleTest.is(e.clientX, 1, "mouse event clientX"); + window.arguments[0].SimpleTest.is(e.clientY, 2, "mouse event clientY"); + // fire the next test on the sub frame + frames[0].runTests(); +} + +window.addEventListener("mousemove",mouseMove); + +</script> +</window> diff --git a/toolkit/content/tests/chrome/window_tooltip.xhtml b/toolkit/content/tests/chrome/window_tooltip.xhtml new file mode 100644 index 0000000000..6a573f0bd9 --- /dev/null +++ b/toolkit/content/tests/chrome/window_tooltip.xhtml @@ -0,0 +1,375 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Tooltip Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<tooltip id="thetooltip"> + <label id="label" class="tooltip-label" value="This is a tooltip"/> +</tooltip> + +<box id="parent" tooltiptext="Box Tooltip" style="margin: 10px"> + <button id="withtext" label="Tooltip Text" tooltiptext="Button Tooltip" + style="-moz-appearance: none; padding: 0;"/> + <button id="without" label="No Tooltip" style="-moz-appearance: none; padding: 0;"/> + <!-- remove the native theme and borders to avoid some platform + specific sizing differences --> + <button id="withtooltip" label="Tooltip Element" tooltip="thetooltip" + class="plain" style="-moz-appearance: none; padding: 0;"/> +</box> + +<script class="testbody" type="application/javascript"> +<![CDATA[ +/* import-globals-from ../widgets/popup_shared.js */ +var gOriginalWidth = -1; +var gOriginalHeight = -1; +var gButton = null; + +function runTest() +{ + startPopupTests(popupTests); +} + +function checkCoords(event) +{ + // all but one test open the tooltip at the button location offset by 6 + // in each direction. Test 5 opens it at 4 in each direction. + var mod = (gTestIndex == 5) ? 4 : 6; + + var rect = gButton.getBoundingClientRect(); + is(event.clientX, Math.round(rect.left + mod), + "step " + (gTestIndex + 1) + " clientX"); + is(event.clientY, Math.round(rect.top + mod), + "step " + (gTestIndex + 1) + " clientY"); + ok(event.screenX > 0, "step " + (gTestIndex + 1) + " screenX"); + ok(event.screenY > 0, "step " + (gTestIndex + 1) + " screenY"); +} + +var popupTests = [ +{ + testname: "hover tooltiptext attribute", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test() { + gButton = document.getElementById("withtext"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "close tooltip", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip" ], + test() { + disableNonTestMouse(true); + synthesizeMouse(document.documentElement, 2, 2, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover inherited tooltip", + events: [ "popupshowing #tooltip", "popupshown #tooltip" ], + test() { + gButton = document.getElementById("without"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + } +}, +{ + testname: "hover tooltip attribute", + events: [ "popuphiding #tooltip", "popuphidden #tooltip", + "DOMMenuInactive #tooltip", + "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + gExpectedTriggerNode = gButton; + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var tooltip = document.getElementById("thetooltip"); + gExpectedTriggerNode = null; + is(tooltip.triggerNode, gButton, testname + " triggerNode"); + + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = tooltip.getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + gOriginalWidth = rect.right - rect.left; + gOriginalHeight = rect.bottom - rect.top; + } +}, +{ + testname: "click to close tooltip", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "command withtooltip", "DOMMenuInactive thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + synthesizeMouse(gButton, 2, 2, { }); + }, + result(testname) { + var tooltip = document.getElementById("thetooltip"); + is(tooltip.triggerNode, null, testname + " triggerNode"); + } +}, +{ + testname: "hover tooltip after size increased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + var label = document.getElementById("label"); + label.removeAttribute("value"); + label.textContent = "This is a longer tooltip than before\nIt has multiple lines\nIt is testing tooltip sizing\n"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 4), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 4), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + // make sure that the tooltip is larger than it was before by just + // checking against the original height plus an arbitrary 15 pixels + ok(gOriginalWidth + 15 < rect.right - rect.left, testname + " tooltip is wider"); + ok(gOriginalHeight + 15 < rect.bottom - rect.top, testname + " tooltip is taller"); + } +}, +{ + testname: "close tooltip with hidePopup", + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ], + test() { + document.getElementById("thetooltip").hidePopup(); + }, +}, +{ + testname: "hover tooltip after size decreased", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + test() { + var label = document.getElementById("label"); + label.value = "This is a tooltip"; + label.textContent = ""; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.left), + Math.round(buttonrect.left + parseFloat(popupstyle.marginLeft) + 6), + testname + " left position of tooltip"); + is(Math.round(rect.top), + Math.round(buttonrect.top + parseFloat(popupstyle.marginTop) + 6), + testname + " top position of tooltip"); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + is(gOriginalWidth, rect.right - rect.left, testname + " tooltip is original width"); + is(gOriginalHeight, rect.bottom - rect.top, testname + " tooltip is original height"); + } +}, +{ + testname: "hover tooltip at bottom edge of screen", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + condition() { + // Only checking OSX here because on other platforms popups and tooltips behave the same way + // when there's not enough space to show them below (by flipping vertically) + // However, on OSX most popups are not flipped but tooltips are. + return navigator.platform.indexOf("Mac") > -1; + }, + test() { + var buttonRect = document.getElementById("withtext").getBoundingClientRect(); + var windowY = screen.height - + (window.mozInnerScreenY - window.screenY ) - buttonRect.bottom; + + moveWindowTo(window.screenX, windowY, function() { + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + disableNonTestMouse(false); + }); + }, + result(testname) { + var buttonrect = document.getElementById("withtooltip").getBoundingClientRect(); + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + var popupstyle = window.getComputedStyle(document.getElementById("thetooltip")); + + is(Math.round(rect.y + rect.height), + Math.round(buttonrect.top + 4 - parseFloat(popupstyle.marginTop)), + testname + " position of tooltip above button"); + } +}, +{ + testname: "open tooltip for keyclose", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + test() { + gButton = document.getElementById("withtooltip"); + gExpectedTriggerNode = gButton; + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, +}, +{ + testname: "close tooltip with modifiers", + test() { + // Press all of the modifiers; the tooltip should remain open on all platforms. + synthesizeKey("KEY_Shift"); + synthesizeKey("KEY_Control"); + synthesizeKey("KEY_Alt"); + synthesizeKey("KEY_Alt"); + }, + result() { + is(document.getElementById("thetooltip").state, "open", "tooltip still open after modifiers pressed") + } +}, +{ + testname: "close tooltip with key", + events() { + if (navigator.platform.indexOf("Win") > -1) { + return []; + } + return [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ]; + }, + test() { + sendString("a"); + }, + result() { + let expectedState = (navigator.platform.indexOf("Win") > -1) ? "open" : "closed"; + is(document.getElementById("thetooltip").state, expectedState, "tooltip closed after key pressed") + } +}, +{ + testname: "close tooltip with hidePopup again", + condition() { return navigator.platform.indexOf("Win") > -1; }, + events: [ "popuphiding thetooltip", "popuphidden thetooltip", + "DOMMenuInactive thetooltip" ], + test() { + document.getElementById("thetooltip").hidePopup(); + }, +}, +{ + testname: "hover tooltip with long unbreakable text", + events: [ "popupshowing thetooltip", "popupshown thetooltip" ], + autohide: "thetooltip", + test() { + var label = document.getElementById("label"); + label.removeAttribute("value"); + label.textContent = "This_tooltip_contains_no_whitespace_and_is_longer_than_the_maximum_tooltip_width_It_should_wrap_onto_a_second_line_instead_of_being_truncated"; + gButton = document.getElementById("withtooltip"); + disableNonTestMouse(true); + synthesizeMouse(gButton, 2, 2, { type: "mouseover" }); + synthesizeMouse(gButton, 4, 4, { type: "mousemove" }); + synthesizeMouse(gButton, 6, 6, { type: "mousemove" }); + disableNonTestMouse(false); + }, + result(testname) { + var rect = document.getElementById("thetooltip").getBoundingClientRect(); + + var labelrect = document.getElementById("label").getBoundingClientRect(); + ok(labelrect.right < rect.right, testname + " tooltip width"); + ok(labelrect.bottom < rect.bottom, testname + " tooltip height"); + + // make sure that the tooltip contains more than one line of text, by checking + // that the original height has increased by at least 10 pixels + ok(gOriginalHeight + 10 < rect.bottom - rect.top, testname + " tooltip is wrapped"); + } +} +]; + +var waitSteps = 0; +var oldx, oldy; +function moveWindowTo(x, y, callback, arg) +{ + if (!waitSteps) { + oldx = window.screenX; + oldy = window.screenY; + window.moveTo(x, y); + + waitSteps++; + setTimeout(moveWindowTo, 100, x, y, callback, arg); + return; + } + + if (window.screenX == oldx && window.screenY == oldy) { + if (waitSteps++ > 10) { + ok(false, "Window never moved properly to " + x + "," + y); + window.arguments[0].SimpleTest.finish(); + window.close(); + } + + setTimeout(moveWindowTo, 100, x, y, callback, arg); + } + else { + waitSteps = 0; + callback(arg); + } +} + +window.arguments[0].SimpleTest.waitForFocus(runTest, window); +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/chrome/xul_selectcontrol.js b/toolkit/content/tests/chrome/xul_selectcontrol.js new file mode 100644 index 0000000000..310ab5c3fa --- /dev/null +++ b/toolkit/content/tests/chrome/xul_selectcontrol.js @@ -0,0 +1,689 @@ +// This script is used to test elements that implement +// nsIDOMXULSelectControlElement. This currently is the following elements: +// listbox, menulist, radiogroup, richlistbox, tabs +// +// flag behaviours that differ for certain elements +// allow-other-value - alternate values for the value property may be used +// besides those in the list +// other-value-clears-selection - alternative values for the value property +// clears the selected item +// selection-required - an item must be selected in the list, unless there +// aren't any to select +// activate-disabled-menuitem - disabled menuitems can be highlighted +// select-keynav-wraps - key navigation over a selectable list wraps +// select-extended-keynav - home, end, page up and page down keys work to +// navigate over a selectable list +// keynav-leftright - key navigation is left/right rather than up/down +// The win:, mac: and gtk: or other prefixes may be used for platform specific behaviour +var behaviours = { + menu: "win:activate-disabled-menuitem activate-disabled-menuitem-mousemove select-keynav-wraps select-extended-keynav", + menulist: "allow-other-value other-value-clears-selection", + listbox: "select-extended-keynav", + richlistbox: "select-extended-keynav", + radiogroup: "select-keynav-wraps dont-select-disabled allow-other-value", + tabs: "select-extended-keynav mac:select-keynav-wraps allow-other-value selection-required keynav-leftright", +}; + +function behaviourContains(tag, behaviour) { + var platform = "none:"; + if (navigator.platform.includes("Mac")) { + platform = "mac:"; + } else if (navigator.platform.includes("Win")) { + platform = "win:"; + } else if (navigator.platform.includes("X")) { + platform = "gtk:"; + } + + var re = new RegExp( + "\\s" + platform + behaviour + "\\s|\\s" + behaviour + "\\s" + ); + return re.test(" " + behaviours[tag] + " "); +} + +function test_nsIDOMXULSelectControlElement(element, childtag, testprefix) { + var testid = testprefix ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement "; + + // 'initial' - check if the initial state of the element is correct + test_nsIDOMXULSelectControlElement_States( + element, + testid + "initial", + 0, + null, + -1, + "" + ); + + test_nsIDOMXULSelectControlElement_init(element, testid); + + // 'appendItem' - check if appendItem works to add a new item + var firstitem = element.appendItem("First Item", "first"); + is( + firstitem.localName, + childtag, + testid + "appendItem - first item is " + childtag + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "appendItem", + 1, + null, + -1, + "" + ); + + is(firstitem.control, element, testid + "control"); + + // 'selectedIndex' - check if an item may be selected + element.selectedIndex = 0; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex", + 1, + firstitem, + 0, + "first" + ); + + // 'appendItem 2' - check if a second item may be added + var seconditem = element.appendItem("Second Item", "second"); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "appendItem 2", + 2, + firstitem, + 0, + "first" + ); + + // 'selectedItem' - check if the second item may be selected + element.selectedItem = seconditem; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem", + 2, + seconditem, + 1, + "second" + ); + + // 'selectedIndex 2' - check if selectedIndex may be set to -1 to deselect items + var selectionRequired = behaviourContains( + element.localName, + "selection-required" + ); + element.selectedIndex = -1; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex 2", + 2, + selectionRequired ? seconditem : null, + selectionRequired ? 1 : -1, + selectionRequired ? "second" : "" + ); + + // 'selectedItem 2' - check if the selectedItem property may be set to null + element.selectedIndex = 1; + element.selectedItem = null; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem 2", + 2, + selectionRequired ? seconditem : null, + selectionRequired ? 1 : -1, + selectionRequired ? "second" : "" + ); + + // 'getIndexOfItem' - check if getIndexOfItem returns the right index + is( + element.getIndexOfItem(firstitem), + 0, + testid + "getIndexOfItem - first item at index 0" + ); + is( + element.getIndexOfItem(seconditem), + 1, + testid + "getIndexOfItem - second item at index 1" + ); + + var otheritem = element.ownerDocument.createXULElement(childtag); + is( + element.getIndexOfItem(otheritem), + -1, + testid + "getIndexOfItem - other item not found" + ); + + // 'getItemAtIndex' - check if getItemAtIndex returns the right item + is( + element.getItemAtIndex(0), + firstitem, + testid + "getItemAtIndex - index 0 is first item" + ); + is( + element.getItemAtIndex(1), + seconditem, + testid + "getItemAtIndex - index 0 is second item" + ); + is( + element.getItemAtIndex(-1), + null, + testid + "getItemAtIndex - index -1 is null" + ); + is( + element.getItemAtIndex(2), + null, + testid + "getItemAtIndex - index 2 is null" + ); + + // check if setting the value changes the selection + element.value = "first"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value 1", + 2, + firstitem, + 0, + "first" + ); + element.value = "second"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value 2", + 2, + seconditem, + 1, + "second" + ); + // setting the value attribute to one not in the list doesn't change the selection. + // The value is only changed for elements which support having a value other than the + // selection. + element.value = "other"; + var allowOtherValue = behaviourContains( + element.localName, + "allow-other-value" + ); + var otherValueClearsSelection = behaviourContains( + element.localName, + "other-value-clears-selection" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "set value other", + 2, + otherValueClearsSelection ? null : seconditem, + otherValueClearsSelection ? -1 : 1, + allowOtherValue ? "other" : "second" + ); + if (allowOtherValue) { + element.value = ""; + } + + var fourthitem = element.appendItem("Fourth Item", "fourth"); + element.selectedIndex = 0; + fourthitem.disabled = true; + element.selectedIndex = 2; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedIndex disabled", + 3, + fourthitem, + 2, + "fourth" + ); + + element.selectedIndex = 0; + element.selectedItem = fourthitem; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "selectedItem disabled", + 3, + fourthitem, + 2, + "fourth" + ); + + if (element.menupopup) { + element.menupopup.textContent = ""; + } else { + element.textContent = ""; + } +} + +function test_nsIDOMXULSelectControlElement_init(element, testprefix) { + var id = element.id; + element = document.getElementById(id + "-initwithvalue"); + if (element) { + var seconditem = element.getItemAtIndex(1); + test_nsIDOMXULSelectControlElement_States( + element, + testprefix + " value initialization", + 3, + seconditem, + 1, + seconditem.value + ); + } + + element = document.getElementById(id + "-initwithselected"); + if (element) { + var thirditem = element.getItemAtIndex(2); + test_nsIDOMXULSelectControlElement_States( + element, + testprefix + " selected initialization", + 3, + thirditem, + 2, + thirditem.value + ); + } +} + +function test_nsIDOMXULSelectControlElement_States( + element, + testid, + expectedcount, + expecteditem, + expectedindex, + expectedvalue +) { + // need an itemCount property here + var count = element.itemCount; + is(count, expectedcount, testid + " item count"); + is(element.selectedItem, expecteditem, testid + " selectedItem"); + is(element.selectedIndex, expectedindex, testid + " selectedIndex"); + is(element.value, expectedvalue, testid + " value"); + if (element.selectedItem) { + is( + element.selectedItem.selected, + true, + testid + " selectedItem marked as selected" + ); + } +} + +/** test_nsIDOMXULSelectControlElement_UI + * + * Test the UI aspects of an element which implements nsIDOMXULSelectControlElement + * + * Parameters: + * element - element to test + */ +function test_nsIDOMXULSelectControlElement_UI(element, testprefix) { + var testid = testprefix ? testprefix + " " : ""; + testid += element.localName + " nsIDOMXULSelectControlElement UI "; + + if (element.menupopup) { + element.menupopup.textContent = ""; + } else { + element.textContent = ""; + } + + var firstitem = element.appendItem("First Item", "first"); + var seconditem = element.appendItem("Second Item", "second"); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent( + firstitem, + 2, + 2, + {}, + element, + "select", + testid + "mouse select" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select", + 2, + firstitem, + 0, + "first" + ); + + synthesizeMouseExpectEvent( + seconditem, + 2, + 2, + {}, + element, + "select", + testid + "mouse select 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select 2", + 2, + seconditem, + 1, + "second" + ); + + // make sure the element is focused so keyboard navigation will apply + element.selectedIndex = 1; + element.focus(); + + var navLeftRight = behaviourContains(element.localName, "keynav-leftright"); + var backKey = navLeftRight ? "VK_LEFT" : "VK_UP"; + var forwardKey = navLeftRight ? "VK_RIGHT" : "VK_DOWN"; + + // 'key select' - check if keypresses move between items + synthesizeKeyExpectEvent(backKey, {}, element, "select", testid + "key up"); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up", + 2, + firstitem, + 0, + "first" + ); + + var keyWrap = behaviourContains(element.localName, "select-keynav-wraps"); + + var expectedItem = keyWrap ? seconditem : firstitem; + var expectedIndex = keyWrap ? 1 : 0; + var expectedValue = keyWrap ? "second" : "first"; + synthesizeKeyExpectEvent( + backKey, + {}, + keyWrap ? element : null, + "select", + testid + "key up 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up 2", + 2, + expectedItem, + expectedIndex, + expectedValue + ); + + element.selectedIndex = 0; + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down", + 2, + seconditem, + 1, + "second" + ); + + expectedItem = keyWrap ? firstitem : seconditem; + expectedIndex = keyWrap ? 0 : 1; + expectedValue = keyWrap ? "first" : "second"; + synthesizeKeyExpectEvent( + forwardKey, + {}, + keyWrap ? element : null, + "select", + testid + "key down 2" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down 2", + 2, + expectedItem, + expectedIndex, + expectedValue + ); + + var thirditem = element.appendItem("Third Item", "third"); + var fourthitem = element.appendItem("Fourth Item", "fourth"); + if (behaviourContains(element.localName, "select-extended-keynav")) { + element.appendItem("Fifth Item", "fifth"); + var sixthitem = element.appendItem("Sixth Item", "sixth"); + + synthesizeKeyExpectEvent( + "VK_END", + {}, + element, + "select", + testid + "key end" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key end", + 6, + sixthitem, + 5, + "sixth" + ); + + synthesizeKeyExpectEvent( + "VK_HOME", + {}, + element, + "select", + testid + "key home" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key home", + 6, + firstitem, + 0, + "first" + ); + + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + {}, + element, + "select", + testid + "key page down" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page down", + 6, + fourthitem, + 3, + "fourth" + ); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + {}, + element, + "select", + testid + "key page down to end" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page down to end", + 6, + sixthitem, + 5, + "sixth" + ); + + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + {}, + element, + "select", + testid + "key page up" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page up", + 6, + thirditem, + 2, + "third" + ); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + {}, + element, + "select", + testid + "key page up to start" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key page up to start", + 6, + firstitem, + 0, + "first" + ); + + element.getItemAtIndex(5).remove(); + element.getItemAtIndex(4).remove(); + } + + // now test whether a disabled item works. + element.selectedIndex = 0; + seconditem.disabled = true; + + var dontSelectDisabled = behaviourContains( + element.localName, + "dont-select-disabled" + ); + + // 'mouse select' - check if clicking an item selects it + synthesizeMouseExpectEvent( + seconditem, + 2, + 2, + {}, + element, + dontSelectDisabled ? "!select" : "select", + testid + "mouse select disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "mouse select disabled", + 4, + dontSelectDisabled ? firstitem : seconditem, + dontSelectDisabled ? 0 : 1, + dontSelectDisabled ? "first" : "second" + ); + + if (dontSelectDisabled) { + // test whether disabling an item won't allow it to be selected + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled", + 4, + thirditem, + 2, + "third" + ); + + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled", + 4, + firstitem, + 0, + "first" + ); + + element.selectedIndex = 2; + firstitem.disabled = true; + + synthesizeKeyExpectEvent( + backKey, + {}, + keyWrap ? element : null, + "select", + testid + "key up disabled 2" + ); + expectedItem = keyWrap ? fourthitem : thirditem; + expectedIndex = keyWrap ? 3 : 2; + expectedValue = keyWrap ? "fourth" : "third"; + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled 2", + 4, + expectedItem, + expectedIndex, + expectedValue + ); + } else { + // in this case, disabled items should behave the same as non-disabled items. + element.selectedIndex = 0; + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled", + 4, + seconditem, + 1, + "second" + ); + synthesizeKeyExpectEvent( + forwardKey, + {}, + element, + "select", + testid + "key down disabled again" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key down disabled again", + 4, + thirditem, + 2, + "third" + ); + + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled", + 4, + seconditem, + 1, + "second" + ); + synthesizeKeyExpectEvent( + backKey, + {}, + element, + "select", + testid + "key up disabled again" + ); + test_nsIDOMXULSelectControlElement_States( + element, + testid + "key up disabled again", + 4, + firstitem, + 0, + "first" + ); + } +} diff --git a/toolkit/content/tests/mochitest/file_mousecapture.html b/toolkit/content/tests/mochitest/file_mousecapture.html new file mode 100644 index 0000000000..bc3d1869f6 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture.html @@ -0,0 +1 @@ +<html><p>One</p><p style='margin-top: 200px;'>Two</p><p style='margin-top: 4000px'>This is some text</p></html> diff --git a/toolkit/content/tests/mochitest/file_mousecapture2.html b/toolkit/content/tests/mochitest/file_mousecapture2.html new file mode 100644 index 0000000000..e746cf7807 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture2.html @@ -0,0 +1,8 @@ +<html><p>One</p> +<p id="myStyle">Two</p> +<p style='margin-top: 4000px'>This is some text</p> +</html> + +<script type="application/javascript"> +document.getElementById("myStyle").style.marginTop = new URL(location.href).searchParams.get("topPos"); +</script> diff --git a/toolkit/content/tests/mochitest/file_mousecapture3.html b/toolkit/content/tests/mochitest/file_mousecapture3.html new file mode 100644 index 0000000000..ee3fad455a --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture3.html @@ -0,0 +1 @@ +<body style='font-size: 40pt;'>.<b id='b'>This</b> is some text<div id='fixed' style='position: fixed; left: 55px; top: 5px; width: 10px; height: 10px'>.</div></body> diff --git a/toolkit/content/tests/mochitest/file_mousecapture4.html b/toolkit/content/tests/mochitest/file_mousecapture4.html new file mode 100644 index 0000000000..fbf30589c1 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture4.html @@ -0,0 +1 @@ +<frameset cols='50%, 50%'><frame src='about:blank'><frame src='about:blank'></frameset> diff --git a/toolkit/content/tests/mochitest/file_mousecapture5.html b/toolkit/content/tests/mochitest/file_mousecapture5.html new file mode 100644 index 0000000000..bc632b6156 --- /dev/null +++ b/toolkit/content/tests/mochitest/file_mousecapture5.html @@ -0,0 +1 @@ +<input id='input' onfocus='this.style.display = "none"' style='float: left;'> diff --git a/toolkit/content/tests/mochitest/gizmo.mp4 b/toolkit/content/tests/mochitest/gizmo.mp4 Binary files differnew file mode 100644 index 0000000000..87efad5ade --- /dev/null +++ b/toolkit/content/tests/mochitest/gizmo.mp4 diff --git a/toolkit/content/tests/mochitest/mochitest.toml b/toolkit/content/tests/mochitest/mochitest.toml new file mode 100644 index 0000000000..627c9dfbd0 --- /dev/null +++ b/toolkit/content/tests/mochitest/mochitest.toml @@ -0,0 +1,26 @@ +[DEFAULT] + +["test_autocomplete_change_after_focus.html"] +skip-if = ["os == 'android'"] + +["test_bug1407085.html"] + +["test_mousecapture.xhtml"] +allow_xul_xbl = true +support-files = [ + "file_mousecapture.html", + "file_mousecapture2.html", + "file_mousecapture3.html", + "file_mousecapture4.html", + "file_mousecapture5.html", +] +skip-if = [ + "os == 'android'", + "xorigin", # SecurityError: Permission denied to access property "scrollX" on cross-origin object at runTests@http://mochi.test:8888/tests/toolkit/content/tests/mochitest/test_mousecapture.xhtml:170:17, inconsistent fail/pass + "http3", + "http2", +] + +["test_video_control_no_control_overlay.html"] +support-files = ["gizmo.mp4"] +run-if = ["os == 'android'"] diff --git a/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html new file mode 100644 index 0000000000..fe4a6ee67c --- /dev/null +++ b/toolkit/content/tests/mochitest/test_autocomplete_change_after_focus.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998893 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998893 - Ensure that input.value changes affect autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + /** Test for Bug 998893 **/ + add_task(async function waitForFocus() { + await new Promise(resolve => SimpleTest.waitForFocus(resolve)); + }); + + add_task(async function setup() { + await new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + const {FormHistory} = ChromeUtils.importESModule( + "resource://gre/modules/FormHistory.sys.mjs" + ); + FormHistory.update([ + { op: "bump", fieldname: "field1", value: "Default text option" }, + { op: "bump", fieldname: "field1", value: "New value option" }, + ]).then(() => { + sendAsyncMessage("Test:Resume"); + }); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + }); + + add_task(async function runTest() { + let promisePopupShown = new Promise(resolve => { + let chromeScript = SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + let window = Services.wm.getMostRecentWindow("navigator:browser"); + let popup = window.document.getElementById("PopupAutoComplete"); + popup.addEventListener("popupshown", function() { + sendAsyncMessage("Test:Resume"); + }, {once: true}); + }); + + chromeScript.addMessageListener("Test:Resume", function resumeListener() { + chromeScript.removeMessageListener("Test:Resume", resumeListener); + chromeScript.destroy(); + resolve(); + }); + }); + + let field = document.getElementById("field1"); + + let promiseFieldFocus = new Promise(resolve => { + field.addEventListener("focus", function onFocus() { + info("field focused"); + field.value = "New value"; + sendKey("DOWN"); + resolve(); + }); + }); + + let handleEnterPromise = new Promise(resolve => { + function handleEnter(evt) { + if (evt.keyCode != KeyEvent.DOM_VK_RETURN) { + return; + } + info("RETURN received for phase: " + evt.eventPhase); + is(evt.target.value, "New value option", "Check that the correct autocomplete entry was used"); + resolve(); + } + + SpecialPowers.addSystemEventListener(field, "keypress", handleEnter, true); + }); + + field.focus(); + + await promiseFieldFocus; + + await promisePopupShown; + + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); + + await handleEnterPromise; + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998893">Mozilla Bug 998893</a> +<p id="display"><input id="field1" value="Default text"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/mochitest/test_bug1407085.html b/toolkit/content/tests/mochitest/test_bug1407085.html new file mode 100644 index 0000000000..17fc2f8a3b --- /dev/null +++ b/toolkit/content/tests/mochitest/test_bug1407085.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 1407085</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <input id="input" value="original value"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = document.getElementById("input"); + input.focus(); + input.addEventListener("keydown", () => { + input.value = "new value"; + }, { once: true }); + synthesizeKey("KEY_Escape"); + is(input.value, "new value", + "New <input> value changed by an Escape key event listener shouldn't be " + + "overwritten by original value even if Escape key is pressed"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/mochitest/test_mousecapture.xhtml b/toolkit/content/tests/mochitest/test_mousecapture.xhtml new file mode 100644 index 0000000000..15576ab45b --- /dev/null +++ b/toolkit/content/tests/mochitest/test_mousecapture.xhtml @@ -0,0 +1,351 @@ +<?xml version="1.0"?> +<!DOCTYPE HTML> +<html xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Mouse Capture Tests</title> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"/><div id="content" style="display: none"/><pre id="test"/> + +<script><![CDATA[ + +SimpleTest.expectAssertions(6, 12); + +SimpleTest.waitForExplicitFinish(); + +const TEST_PATH = location.href.replace("test_mousecapture.xhtml", ""); + +var captureRetargetMode = false; +var cachedMouseDown = null; +var previousWidth = 0, originalWidth = 0; +var loadInWindow = false; + +/** + * We'll make sure to suppress any dragstart events and prevent them + * from reaching the widget layer to avoid this assertion: + * https://searchfox.org/mozilla-central/rev/47aea2f603cc18144afcedbd604a418f11e90f9b/widget/nsBaseDragService.cpp#341-355 + */ +addEventListener("dragstart", e => { + e.preventDefault(); + e.stopPropagation(); +}); + +function splitterCallback(adjustment) { + var newWidth = parseInt($("leftbox").style.width); // getBoundingClientRect().width; + var expectedWidth = previousWidth + adjustment; + if (expectedWidth > $("splitterbox").getBoundingClientRect().width) + expectedWidth = $("splitterbox").getBoundingClientRect().width - $("splitter").getBoundingClientRect().width; + is(newWidth, expectedWidth, "splitter left box size (" + adjustment + ")"); + previousWidth = newWidth; +} + +function selectionCallback(adjustment) { + if (adjustment == 4000) { + is(frames[0].getSelection().toString(), "This is some text", "selection after drag (" + adjustment + ")"); + ok(frames[0].scrollY > 40, "selection caused scroll down (" + adjustment + ")"); + } else { + if (adjustment == 0) { + is(frames[0].getSelection().toString(), ".", "selection after drag (" + adjustment + ")"); + } + is(frames[0].scrollY, 0, "selection scrollY (" + adjustment + ")"); + } +} + +function framesetCallback(adjustment) { + var newWidth = frames[1].frames[0].document.documentElement.clientWidth; + var expectedWidth = originalWidth + adjustment; + if (adjustment == 0) + expectedWidth = originalWidth - 12; + else if (expectedWidth >= 4000) + expectedWidth = originalWidth * 2 - 2; + + ok(Math.abs(newWidth - expectedWidth) <= 1, "frameset after drag (" + adjustment + "), new width " + newWidth + ", expected " + expectedWidth); +} + +var otherWindow = null; + +function selectionScrollCheck() { + var element = otherWindow.document.documentElement; + + var count = 0; + let previousScrollPosition = 0; + + function selectionScrollDone() { + // Don't count unchanged scroll position. + // There's a bug that scroll events get fired during reconstructing scroll + // frames even if the scroll position is unchanged (bug 1825470). We need to + // ignore the unchanged scroll events here. + if (otherWindow.scrollY == previousScrollPosition) { + return; + } + previousScrollPosition = otherWindow.scrollY; + + // wait for 6 scroll events to occur + if (count++ < 6) { + return; + } + + otherWindow.removeEventListener("scroll", selectionScrollDone); + + var selectedText = otherWindow.getSelection().toString().replace(/\r/g, ""); + // We trim the end of the string because, per the below comment, we might + // select additional newlines, and that's OK. + is(selectedText.trimEnd(), "One\n\nTwo", "text is selected"); + + // should have scrolled 20 pixels from the mousemove above and at least 6 + // extra 20-pixel increments from the selection scroll timer. "At least 6" + // because we waited for 6 scroll events but multiple scrolls could get + // coalesced into a single scroll event, and paints could be delayed when + // the window loads when the compositor is busy. As a result, we have no + // real guarantees about the upper bound here, and as the upper bound is + // not important for what we're testing here, we don't check it. + var scrollY = otherWindow.scrollY; + info(`Scrolled ${scrollY} pixels`); + ok(scrollY >= 140, "selection scroll position after timer is at least 140"); + ok((scrollY % 20) == 0, "selection scroll position after timer is multiple of 20"); + + synthesizeMouse(element, 4, otherWindow.innerHeight + 25, { type: "mouseup" }, otherWindow); + disableNonTestMouseEvents(false); + otherWindow.close(); + + if (loadInWindow) { + SimpleTest.finish(); + } else { + // now try again, but open the page in a new window + loadInWindow = true; + synthesizeMouse(document.getElementById("custom"), 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area + otherWindow = window.open(TEST_PATH + "file_mousecapture.html", "_blank", "width=200,height=200,scrollbars=yes"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); + } + } + + SimpleTest.executeSoon(function() { + disableNonTestMouseEvents(true); + synthesizeMouse(element, 2, 2, { type: "mousedown" }, otherWindow); + synthesizeMouse(element, 100, otherWindow.innerHeight + 20, { type: "mousemove" }, otherWindow); + otherWindow.addEventListener("scroll", selectionScrollDone); + }); +} + +function runTests() { + previousWidth = $("leftbox").getBoundingClientRect().width; + runCaptureTest($("splitter"), splitterCallback); + + var custom = document.getElementById("custom"); + runCaptureTest(custom); + + synthesizeMouseExpectEvent($("rightbox"), 2, 2, { type: "mousemove" }, + $("rightbox"), "mousemove", "setCapture and releaseCapture"); + + custom.setCapture(); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture fails on non mousedown"); + + var custom2 = document.getElementById("custom2"); + synthesizeMouse(custom2, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "document.releaseCapture releases capture"); + + var custom3 = document.getElementById("custom3"); + synthesizeMouse(custom3, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture releases capture"); + + var custom4 = document.getElementById("custom4"); + synthesizeMouse(custom4, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + custom4, "mousemove", "element.releaseCapture during mousemove before releaseCapture"); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "element.releaseCapture during mousemove after releaseCapture"); + + var custom5 = document.getElementById("custom5"); + runCaptureTest(custom5); + captureRetargetMode = true; + runCaptureTest(custom5); + captureRetargetMode = false; + + var custom6 = document.getElementById("custom6"); + synthesizeMouse(custom6, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + $("leftbox"), "mousemove", "setCapture only works on elements in documents"); + synthesizeMouse(custom6, 2, 2, { type: "mouseup" }); + + // test that mousedown on an image with setCapture followed by a big enough + // mouse move does not start a drag (bug 517737) + var image = document.getElementById("image"); + image.scrollIntoView(); + synthesizeMouse(image, 2, 2, { type: "mousedown" }); + synthesizeMouseExpectEvent($("leftbox"), 2, 2, { type: "mousemove" }, + image, "mousemove", "setCapture works on images"); + synthesizeMouse(image, 2, 2, { type: "mouseup" }); + + window.scroll(0, 0); + + // save scroll + var scrollX = parent ? parent.scrollX : 0; + var scrollY = parent ? parent.scrollY : 0; + + // restore scroll + if (parent) parent.scroll(scrollX, scrollY); + +// frames[0].getSelection().collapseToStart(); + + var body = frames[0].document.body; + var fixed = frames[0].document.getElementById("fixed"); + function captureOnBody() { body.setCapture(); } + body.addEventListener("mousedown", captureOnBody, true); + synthesizeMouse(body, 8, 8, { type: "mousedown" }, frames[0]); + body.removeEventListener("mousedown", captureOnBody, true); + synthesizeMouseExpectEvent(fixed, 2, 2, { type: "mousemove" }, + fixed, "mousemove", "setCapture on body retargets to root node", frames[0]); + synthesizeMouse(body, 8, 8, { type: "mouseup" }, frames[0]); + + previousWidth = frames[1].frames[0].document.documentElement.clientWidth; + originalWidth = previousWidth; + runCaptureTest(frames[1].document.documentElement.lastElementChild, framesetCallback); + + // ensure that clicking on an element where the frame disappears doesn't crash + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mousedown" }, frames[2]); + synthesizeMouse(frames[2].document.getElementById("input"), 8, 8, { type: "mouseup" }, frames[2]); + + var select = document.getElementById("select"); + select.scrollIntoView(); + + synthesizeMouse(document.getElementById("option3"), 2, 2, { type: "mousedown" }); + synthesizeMouse(document.getElementById("option3"), 2, 1000, { type: "mousemove" }); + is(select.selectedIndex, 2, "scroll select"); + synthesizeMouse(document.getElementById("select"), 2, 2, { type: "mouseup" }); + window.scroll(0, 0); + + synthesizeMouse(custom, 2, 2, { type: "mousedown" }); + + // check to ensure that selection dragging scrolls the right scrollable area. + // This should open the page in a new tab. + + var topPos = window.innerHeight; + otherWindow = window.open(TEST_PATH + "file_mousecapture2.html?topPos=" + topPos, "_blank"); + SimpleTest.waitForFocus(selectionScrollCheck, otherWindow); +} + +function runCaptureTest(element, callback) { + var expectedTarget = null; + + // ownerGlobal doesn't exist in content privileged windows. + // eslint-disable-next-line mozilla/use-ownerGlobal + var win = element.ownerDocument.defaultView; + + function mouseMoved(event) { + is(event.originalTarget, expectedTarget, + expectedTarget.id + " target for point " + event.clientX + "," + event.clientY); + } + win.addEventListener("mousemove", mouseMoved); + + expectedTarget = element; + + var basepoint = element.localName == "frameset" ? 50 : 2; + synthesizeMouse(element, basepoint, basepoint, { type: "mousedown" }, win); + + // in setCapture(true) mode, all events should fire on custom5. In + // setCapture(false) mode, events can fire at a descendant + if (expectedTarget == $("custom5") && !captureRetargetMode) + expectedTarget = $("custom5spacer"); + + // releaseCapture should do nothing for an element which isn't capturing + $("splitterbox").releaseCapture(); + + synthesizeMouse(element, basepoint + 2, basepoint + 2, { type: "mousemove" }, win); + if (callback) + callback(2); + + if (expectedTarget == $("custom5spacer") && !captureRetargetMode) + expectedTarget = $("custom5inner"); + + if (element.id == "b") { + var tooltip = document.getElementById("tooltip"); + tooltip.openPopup(); + tooltip.hidePopup(); + } + + synthesizeMouse(element, basepoint + 25, basepoint + 25, { type: "mousemove" }, win); + if (callback) + callback(25); + + expectedTarget = element.localName == "b" ? win.document.documentElement : element; + synthesizeMouse(element, basepoint + 4000, basepoint + 4000, { type: "mousemove" }, win); + if (callback) + callback(4000); + synthesizeMouse(element, basepoint - 12, basepoint - 12, { type: "mousemove" }, win); + if (callback) + callback(-12); + + expectedTarget = element.localName == "frameset" ? element : win.document.documentElement; + synthesizeMouse(element, basepoint + 30, basepoint + 30, { type: "mouseup" }, win); + synthesizeMouse(win.document.documentElement, 2, 2, { type: "mousemove" }, win); + if (callback) + callback(0); + + win.removeEventListener("mousemove", mouseMoved); +} + +SimpleTest.waitForFocus(runTests); + +]]> +</script> + +<xul:vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" align="start"> + <tooltip id="tooltip"> + <label value="Test"/> + </tooltip> + + <hbox id="splitterbox" style="margin-top: 5px;" onmousedown="this.setCapture()"> + <hbox id="leftbox" style="width: 100px; flex: 1 auto"/> + <splitter id="splitter" style="width: 5px; height: 5px"/> + <hbox id="rightbox" style="width: 100px; flex: 1 auto"/> + </hbox> + + <vbox id="custom" style="width: 10px; height: 10px" onmousedown="this.setCapture(); cachedMouseDown = event;"/> + <vbox id="custom2" style="width: 10px; height: 10px" onmousedown="this.setCapture(); document.releaseCapture();"/> + <vbox id="custom3" style="width: 10px; height: 10px" onmousedown="this.setCapture(); this.releaseCapture();"/> + <vbox id="custom4" style="width: 10px; height: 10px" onmousedown="this.setCapture();" + onmousemove="this.releaseCapture();"/> + <hbox id="custom5" style="width: 40px; height: 40px" + onmousedown="this.setCapture(captureRetargetMode);"> + <spacer id="custom5spacer" style="width: 5px"/> + <hbox id="custom5inner" style="width: 35px; height: 35px"/> + </hbox> + <vbox id="custom6" style="width: 10px; height: 10px" + onmousedown="document.createElement('hbox').setCapture();"/> +</xul:vbox> + + <iframe style="width: 100px; height: 100px" src="file_mousecapture3.html"/> + <iframe style="width: 100px; height: 100px" src="file_mousecapture4.html"/> + <iframe style="width: 100px; height: 100px" src="file_mousecapture5.html"/> + + <select id="select" xmlns="http://www.w3.org/1999/xhtml" size="4"> + <option id="option1">One</option> + <option id="option2">Two</option> + <option id="option3">Three</option> + <option id="option4">Four</option> + <option id="option5">Five</option> + <option id="option6">Six</option> + <option id="option7">Seven</option> + <option id="option8">Eight</option> + <option id="option9">Nine</option> + <option id="option10">Ten</option> + </select> + + <img id="image" xmlns="http://www.w3.org/1999/xhtml" + onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + ondragstart="ok(false, 'should not get a drag when a setCapture is active');" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC"/> + +</body> + +</html> diff --git a/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html b/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html new file mode 100644 index 0000000000..6a079d77dc --- /dev/null +++ b/toolkit/content/tests/mochitest/test_video_control_no_control_overlay.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Show 'click-to-play' icon on blocked autoplay media</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> +/** + * This test is used to check whether 'click-to-play' icon would be showed + * correctly when autoplay media is blocked. + */ +add_task(async function testShowClickToPlayWhenAutoplayMediaGetsBlocked() { + info(`setting testing pref`); + await SpecialPowers.pushPrefEnv( + {"set": [["media.autoplay.default", 1 /* BLOCKED */]]} + ); + + info(`create video and load resource`); + let video = document.createElement('video'); + video.src = "gizmo.mp4"; + document.body.appendChild(video); + + info(`blocking autoplay would reject media to play`); + ok(await video.play().then(_ => false, _ => true), "Play got rejected"); + + info(`'click-to-play' should display when autoplay media is blocked`); + const button = SpecialPowers.wrap(video).openOrClosedShadowRoot.querySelector(".clickToPlay"); + ok(!button.hidden, "Click-to-play button is not hidden"); +}); + +</script> +</head> +<body> +</body> +</html> diff --git a/toolkit/content/tests/moz.build b/toolkit/content/tests/moz.build new file mode 100644 index 0000000000..682ca7e3e8 --- /dev/null +++ b/toolkit/content/tests/moz.build @@ -0,0 +1,22 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"] + +BROWSER_CHROME_MANIFESTS += [ + "browser/browser.toml", + "browser/datetime/browser.toml", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "chrome/chrome.toml", + "widgets/chrome.toml", +] + +MOCHITEST_MANIFESTS += [ + "mochitest/mochitest.toml", + "widgets/mochitest.toml", +] diff --git a/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html b/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html new file mode 100644 index 0000000000..4bcac35d84 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-dynamically-change-small-width-ref.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio controls></audio> +</body> diff --git a/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html b/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html new file mode 100644 index 0000000000..0e9059541a --- /dev/null +++ b/toolkit/content/tests/reftests/audio-dynamically-change-small-width.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio id="tweakme" controls></audio> + + <script> + function doTest() { + setTimeout(() => { + let tweakme = document.getElementById("tweakme"); + + // Make the audio element extremely skinny, flush layout, and then revert + // that change: + tweakme.style.width = "1px"; + tweakme.offsetHeight; // flush layout + tweakme.style.width = ""; + tweakme.offsetHeight; // flush layout + + document.documentElement.removeAttribute("class"); + }, 300); + } + + window.addEventListener("MozReftestInvalidate", doTest); + </script> +</body> diff --git a/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html b/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html new file mode 100644 index 0000000000..7731e5ced6 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-bogus-url-ref.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio controls></audio> +</body> +</html> diff --git a/toolkit/content/tests/reftests/audio-with-bogus-url.html b/toolkit/content/tests/reftests/audio-with-bogus-url.html new file mode 100644 index 0000000000..d6bf7ff861 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-bogus-url.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } +</style> +</head> +<body> + <audio src="bogus.mp3" controls></audio> +</body> +</html> diff --git a/toolkit/content/tests/reftests/audio-with-padding-ref.html b/toolkit/content/tests/reftests/audio-with-padding-ref.html new file mode 100644 index 0000000000..b0fe3fa6e5 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-padding-ref.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + #wrapper { + padding: 20px; + } +</style> +</head> +<body> + <div id="wrapper"> + <audio controls></audio> + <div> +</body> diff --git a/toolkit/content/tests/reftests/audio-with-padding.html b/toolkit/content/tests/reftests/audio-with-padding.html new file mode 100644 index 0000000000..c18b746374 --- /dev/null +++ b/toolkit/content/tests/reftests/audio-with-padding.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + audio { + padding: 20px; + } +</style> +</head> +<body> + <audio controls></audio> +</body> diff --git a/toolkit/content/tests/reftests/foo.vtt b/toolkit/content/tests/reftests/foo.vtt new file mode 100644 index 0000000000..b533895c60 --- /dev/null +++ b/toolkit/content/tests/reftests/foo.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00:00.000 --> 00:00:05.000 +Foo diff --git a/toolkit/content/tests/reftests/reftest.list b/toolkit/content/tests/reftests/reftest.list new file mode 100644 index 0000000000..d1e8ce3dbd --- /dev/null +++ b/toolkit/content/tests/reftests/reftest.list @@ -0,0 +1,4 @@ +== videocontrols-dynamically-add-cc.html videocontrols-dynamically-add-cc-ref.html +== audio-with-bogus-url.html audio-with-bogus-url-ref.html +== audio-dynamically-change-small-width.html audio-dynamically-change-small-width-ref.html +== audio-with-padding.html audio-with-padding-ref.html diff --git a/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html new file mode 100644 index 0000000000..1dcf4949a6 --- /dev/null +++ b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc-ref.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + video { + width: 320px; + height: 240px; + } + + #mask { + position: absolute; + z-index: 3; + width: 320px; + height: 200px; + background-color: green; + top: 0; + left: 0; + } +</style> +</head> +<body> + <video id="vid" controls> + <track kind="subtitles" src="foo.vtt" srclang="en"> + </video> + <div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html new file mode 100644 index 0000000000..e3d0912a51 --- /dev/null +++ b/toolkit/content/tests/reftests/videocontrols-dynamically-add-cc.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<style> + html, body { + margin: 0; + padding: 0; + } + + video { + width: 320px; + height:240px; + } + + #mask { + position: absolute; + z-index: 3; + width: 320px; + height: 200px; + background-color: green; + top: 0; + left: 0; + } + + #vid-preload-image { + visibility: hidden; + } +</style> +<script> + function addCCToVid(videoElem) { + videoElem.addTextTrack("subtitles", "English", "en"); + } +</script> +</head> +<body> + <video id="vid" controls></video> + <div id="mask"></div> + <!-- Create a hidden video with CC button displayed upfront to decode image + earlier, so that the CC image will be ready to paint once the track added. --> + <video id="vid-preload-image" controls> + <track kind="subtitles" src="foo.vtt" srclang="en"> + </video> + + <script> + function doTest() { + var vid = document.getElementById("vid"); + + // Videocontrols binding's "addtrack" handler synchronously fires + // "adjustControlSize()" first, and then the layout is ready for + // the reftest snapshot. + vid.textTracks.addEventListener("addtrack", function() { + document.documentElement.removeAttribute("class"); + }); + + addCCToVid(vid); + } + + window.addEventListener("MozReftestInvalidate", doTest); + </script> +</body> +</html> diff --git a/toolkit/content/tests/unit/test_contentAreaUtils.js b/toolkit/content/tests/unit/test_contentAreaUtils.js new file mode 100644 index 0000000000..6ea83db251 --- /dev/null +++ b/toolkit/content/tests/unit/test_contentAreaUtils.js @@ -0,0 +1,80 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function loadUtilsScript() { + /* import-globals-from ../../contentAreaUtils.js */ + Services.scriptloader.loadSubScript( + "chrome://global/content/contentAreaUtils.js" + ); +} + +function test_urlSecurityCheck() { + var nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({}); + + const HTTP_URI = "http://www.mozilla.org/"; + const CHROME_URI = "chrome://browser/content/browser.xhtml"; + const DISALLOW_INHERIT_PRINCIPAL = + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL; + + try { + urlSecurityCheck( + makeURI(HTTP_URI), + nullPrincipal, + DISALLOW_INHERIT_PRINCIPAL + ); + } catch (ex) { + do_throw( + "urlSecurityCheck should not throw when linking to a http uri with a null principal" + ); + } + + // urlSecurityCheck also supports passing the url as a string + try { + urlSecurityCheck(HTTP_URI, nullPrincipal, DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + do_throw( + "urlSecurityCheck failed to handle the http URI as a string (uri spec)" + ); + } + + let shouldThrow = true; + try { + urlSecurityCheck(CHROME_URI, nullPrincipal, DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + shouldThrow = false; + } + if (shouldThrow) { + do_throw( + "urlSecurityCheck should throw when linking to a chrome uri with a null principal" + ); + } +} + +function test_stringBundle() { + // This test verifies that the elements that can be used as file picker title + // keys in the save* functions are actually present in the string bundle. + // These keys are part of the contentAreaUtils.js public API. + var validFilePickerTitleKeys = [ + "SaveImageTitle", + "SaveVideoTitle", + "SaveAudioTitle", + "SaveLinkTitle", + ]; + + for (let filePickerTitleKey of validFilePickerTitleKeys) { + // Just check that the string exists + try { + ContentAreaUtils.stringBundle.GetStringFromName(filePickerTitleKey); + } catch (e) { + do_throw("Error accessing file picker title key: " + filePickerTitleKey); + } + } +} + +function run_test() { + loadUtilsScript(); + test_urlSecurityCheck(); + test_stringBundle(); +} diff --git a/toolkit/content/tests/unit/xpcshell.toml b/toolkit/content/tests/unit/xpcshell.toml new file mode 100644 index 0000000000..237183037e --- /dev/null +++ b/toolkit/content/tests/unit/xpcshell.toml @@ -0,0 +1,4 @@ +[DEFAULT] +head = "" + +["test_contentAreaUtils.js"] diff --git a/toolkit/content/tests/widgets/audio.ogg b/toolkit/content/tests/widgets/audio.ogg Binary files differnew file mode 100644 index 0000000000..a553c23e73 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.ogg diff --git a/toolkit/content/tests/widgets/audio.wav b/toolkit/content/tests/widgets/audio.wav Binary files differnew file mode 100644 index 0000000000..c6fd5cb869 --- /dev/null +++ b/toolkit/content/tests/widgets/audio.wav diff --git a/toolkit/content/tests/widgets/chrome.toml b/toolkit/content/tests/widgets/chrome.toml new file mode 100644 index 0000000000..af2c778947 --- /dev/null +++ b/toolkit/content/tests/widgets/chrome.toml @@ -0,0 +1,68 @@ +[DEFAULT] +skip-if = ["os == 'android'"] +support-files = [ + "tree_shared.js", + "popup_shared.js", + "window_label_checkbox.xhtml", + "window_menubar.xhtml", + "seek_with_sound.ogg", +] +prefs = ["app.support.baseURL='https://support.mozilla.org/'"] + +["test_contextmenu_menugroup.xhtml"] +skip-if = ["os == 'linux'"] # Bug 1115088 + +["test_contextmenu_nested.xhtml"] +skip-if = ["os == 'linux'"] # Bug 1116215 + +["test_editor_currentURI.xhtml"] + +["test_label_checkbox.xhtml"] + +["test_menubar.xhtml"] +skip-if = ["os == 'mac'"] + +["test_moz_button_group.html"] + +["test_moz_card.html"] + +["test_moz_five_star.html"] + +["test_moz_label.html"] + +["test_moz_message_bar.html"] + +["test_moz_support_link.html"] + +["test_moz_toggle.html"] + +["test_panel_item_accesskey.html"] + +["test_panel_list_accessibility.html"] + +["test_panel_list_anchoring.html"] + +["test_panel_list_in_xul_panel.html"] + +["test_panel_list_min_width_from_anchor.html"] + +["test_popupanchor.xhtml"] +skip-if = [ + "os == 'linux' && os_version == '18.04'", # Bug 1335894 perma-fail on linux 16.04 + "verify && os == 'win'", +] + +["test_popupreflows.xhtml"] + +["test_tree_column_reorder.xhtml"] + +["test_videocontrols_focus.html"] +support-files = [ + "head.js", + "video.ogg", +] +skip-if = [ + "os == 'android'", + "os == 'linux' && debug", # Bug 1765783 +] +["test_videocontrols_onclickplay.html"] diff --git a/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..56917b69ac --- /dev/null +++ b/toolkit/content/tests/widgets/file_videocontrols_jsdisabled.html @@ -0,0 +1,2 @@ +<video src="seek_with_sound.ogg" controls autoplay=true></video> +<script>window.testExpando = true;</script> diff --git a/toolkit/content/tests/widgets/head.js b/toolkit/content/tests/widgets/head.js new file mode 100644 index 0000000000..d7473fa92d --- /dev/null +++ b/toolkit/content/tests/widgets/head.js @@ -0,0 +1,67 @@ +"use strict"; + +const InspectorUtils = SpecialPowers.InspectorUtils; + +var tests = []; + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function () { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function () { + clearInterval(interval); + nextTest(); + }; +} + +function getElementWithinVideo(video, aValue) { + const shadowRoot = SpecialPowers.wrap(video).openOrClosedShadowRoot; + return shadowRoot.getElementById(aValue); +} + +/** + * Runs querySelectorAll on an element's shadow root. + * @param {Element} element + * @param {string} selector + */ +function shadowRootQuerySelectorAll(element, selector) { + const shadowRoot = SpecialPowers.wrap(element).openOrClosedShadowRoot; + return shadowRoot?.querySelectorAll(selector); +} + +function executeTests() { + return tests + .map(fn => () => new Promise(fn)) + .reduce((promise, task) => promise.then(task), Promise.resolve()); +} + +function once(target, name, cb) { + let p = new Promise(function (resolve, reject) { + target.addEventListener( + name, + function () { + resolve(); + }, + { once: true } + ); + }); + if (cb) { + p.then(cb); + } + return p; +} diff --git a/toolkit/content/tests/widgets/image-zh.png b/toolkit/content/tests/widgets/image-zh.png Binary files differnew file mode 100644 index 0000000000..944a12d39e --- /dev/null +++ b/toolkit/content/tests/widgets/image-zh.png diff --git a/toolkit/content/tests/widgets/image.png b/toolkit/content/tests/widgets/image.png Binary files differnew file mode 100644 index 0000000000..3faa11b221 --- /dev/null +++ b/toolkit/content/tests/widgets/image.png diff --git a/toolkit/content/tests/widgets/mochitest.toml b/toolkit/content/tests/widgets/mochitest.toml new file mode 100644 index 0000000000..7e20352256 --- /dev/null +++ b/toolkit/content/tests/widgets/mochitest.toml @@ -0,0 +1,110 @@ +[DEFAULT] +support-files = [ + "audio.wav", + "audio.ogg", + "file_videocontrols_jsdisabled.html", + "image.png", + "image-zh.png", + "seek_with_sound.ogg", + "video.ogg", + "head.js", + "tree_shared.js", + "test-webvtt-1.vtt", + "test-webvtt-2.vtt", + "videocontrols_direction-1-ref.html", + "videocontrols_direction-1a.html", + "videocontrols_direction-1b.html", + "videocontrols_direction-1c.html", + "videocontrols_direction-1d.html", + "videocontrols_direction-1e.html", + "videocontrols_direction-2-ref.html", + "videocontrols_direction-2a.html", + "videocontrols_direction-2b.html", + "videocontrols_direction-2c.html", + "videocontrols_direction-2d.html", + "videocontrols_direction-2e.html", + "videocontrols_direction_test.js", + "videomask.css", +] + +["test_audiocontrols_dimensions.html"] + +["test_audiocontrols_fullscreen.html"] + +["test_bug898940.html"] + +["test_bug1654500.html"] + +["test_image_recognition.html"] +run-if = ["os == 'mac'"] # Mac only feature. + +["test_image_recognition_unsupported.html"] +skip-if = ["os == 'mac'"] + +["test_image_recognition_zh.html"] +run-if = ["os == 'mac' && os_version != '10.15'"] # Mac only feature, requires > 10.15 to support multilingual results. + +["test_mousecapture_area.html"] + +["test_nac_mutations.html"] + +["test_panel_list_shadow_node_anchor.html"] +support-files = [ + "../../widgets/panel-list/panel-item.css", + "../../widgets/panel-list/panel-list.js", + "../../widgets/panel-list/panel-list.css", +] + +["test_ua_widget_elementFromPoint.html"] + +["test_ua_widget_sandbox.html"] + +["test_ua_widget_unbind.html"] + +["test_videocontrols.html"] +tags = "fullscreen" +skip-if = [ + "os == 'android'", #TIMED_OUT #Bug 1484210 + "os == 'linux'", #TIMED_OUT #Bug 1511256 +] + +["test_videocontrols_audio.html"] + +["test_videocontrols_audio_direction.html"] +skip-if = ["xorigin"] # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently + +["test_videocontrols_clickToPlay_ariaLabel.html"] + +["test_videocontrols_closed_caption_menu.html"] + +["test_videocontrols_error.html"] + +["test_videocontrols_iframe_fullscreen.html"] + +["test_videocontrols_jsdisabled.html"] +skip-if = ["os == 'android'"] # bug 1272646 + +["test_videocontrols_keyhandler.html"] +skip-if = [ + "os == 'android'", #Bug 1366957 + "os == 'linux'", #Bug 1366957 +] + +["test_videocontrols_scrubber_position.html"] + +["test_videocontrols_scrubber_position_nopreload.html"] + +["test_videocontrols_size.html"] + +["test_videocontrols_standalone.html"] +skip-if = ["os == 'android'"] # bug 1075573 + +["test_videocontrols_video_direction.html"] +skip-if = [ + "os == 'win'", + "xorigin", # Rendering of reftest videocontrols_direction-2a.html should not be different to the reference, fails/passes inconsistently +] + +["test_videocontrols_video_noaudio.html"] + +["test_videocontrols_vtt.html"] diff --git a/toolkit/content/tests/widgets/popup_shared.js b/toolkit/content/tests/widgets/popup_shared.js new file mode 100644 index 0000000000..0f093193c9 --- /dev/null +++ b/toolkit/content/tests/widgets/popup_shared.js @@ -0,0 +1,602 @@ +/* + * This script is used for menu and popup tests. Call startPopupTests to start + * the tests, passing an array of tests as an argument. Each test is an object + * with the following properties: + * testname - name of the test + * test - function to call to perform the test + * events - a list of events that are expected to be fired in sequence + * as a result of calling the 'test' function. This list should be + * an array of strings of the form "eventtype targetid" where + * 'eventtype' is the event type and 'targetid' is the id of + * target of the event. This function will be passed two + * arguments, the testname and the step argument. + * Alternatively, events may be a function which returns the array + * of events. This can be used when the events vary per platform. + * result - function to call after all the events have fired to check + * for additional results. May be null. This function will be + * passed two arguments, the testname and the step argument. + * steps - optional array of values. The test will be repeated for + * each step, passing each successive value within the array to + * the test and result functions + * autohide - if set, should be set to the id of a popup to hide after + * the test is complete. This is a convenience for some tests. + * condition - an optional function which, if it returns false, causes the + * test to be skipped. + * end - used for debugging. Set to true to stop the tests after running + * this one. + */ + +const menuactiveAttribute = "_moz-menuactive"; + +var gPopupTests = null; +var gTestIndex = -1; +var gTestStepIndex = 0; +var gTestEventIndex = 0; +var gActualEvents = []; +var gAutoHide = false; +var gExpectedEventDetails = null; +var gExpectedTriggerNode = null; +var gWindowUtils; +var gPopupWidth = -1, + gPopupHeight = -1; + +function startPopupTests(tests) { + document.addEventListener("popupshowing", eventOccurred); + document.addEventListener("popupshown", eventOccurred); + document.addEventListener("popuphiding", eventOccurred); + document.addEventListener("popuphidden", eventOccurred); + document.addEventListener("command", eventOccurred); + document.addEventListener("DOMMenuItemActive", eventOccurred); + document.addEventListener("DOMMenuItemInactive", eventOccurred); + document.addEventListener("DOMMenuInactive", eventOccurred); + document.addEventListener("DOMMenuBarActive", eventOccurred); + document.addEventListener("DOMMenuBarInactive", eventOccurred); + + // This is useful to explicitly finish a test that shouldn't trigger events. + document.addEventListener("TestDone", eventOccurred); + + gPopupTests = tests; + gWindowUtils = SpecialPowers.getDOMWindowUtils(window); + + goNext(); +} + +if (!window.opener && window.arguments) { + window.opener = window.arguments[0]; +} + +function finish() { + if (window.opener) { + window.close(); + window.opener.SimpleTest.finish(); + return; + } + SimpleTest.finish(); +} + +function ok(condition, message) { + if (window.opener) { + window.opener.SimpleTest.ok(condition, message); + } else { + SimpleTest.ok(condition, message); + } +} + +function info(message) { + if (window.opener) { + window.opener.SimpleTest.info(message); + } else { + SimpleTest.info(message); + } +} + +function is(left, right, message) { + if (window.opener) { + window.opener.SimpleTest.is(left, right, message); + } else { + SimpleTest.is(left, right, message); + } +} + +function disableNonTestMouse(aDisable) { + gWindowUtils.disableNonTestMouseEvents(aDisable); +} + +function eventOccurred(event) { + if (gPopupTests.length <= gTestIndex) { + ok(false, "Extra " + event.type + " event fired"); + return; + } + + var test = gPopupTests[gTestIndex]; + if ("autohide" in test && gAutoHide) { + if (event.type == "DOMMenuInactive") { + gAutoHide = false; + setTimeout(goNextStep, 0); + } + return; + } + + var events = test.events; + if (typeof events == "function") { + events = events(); + } + if (events) { + if (events.length <= gTestEventIndex) { + ok( + false, + "Extra " + + event.type + + " event fired for " + + event.target.id + + " " + + gPopupTests[gTestIndex].testname + ); + return; + } + + gActualEvents.push(`${event.type} ${event.target.id}`); + + var eventitem = events[gTestEventIndex].split(" "); + var matches; + if (eventitem[1] == "#tooltip") { + is( + event.originalTarget.localName, + "tooltip", + test.testname + " event.originalTarget.localName is 'tooltip'" + ); + is( + event.originalTarget.getAttribute("default"), + "true", + test.testname + " event.originalTarget default attribute is 'true'" + ); + matches = + event.originalTarget.localName == "tooltip" && + event.originalTarget.getAttribute("default") == "true"; + } else { + is( + event.type, + eventitem[0], + test.testname + " event type " + event.type + " fired" + ); + is( + event.target.id, + eventitem[1], + test.testname + " event target ID " + event.target.id + ); + matches = eventitem[0] == event.type && eventitem[1] == event.target.id; + } + + var modifiersMask = eventitem[2]; + if (modifiersMask) { + var m = ""; + m += event.altKey ? "1" : "0"; + m += event.ctrlKey ? "1" : "0"; + m += event.shiftKey ? "1" : "0"; + m += event.metaKey ? "1" : "0"; + is(m, modifiersMask, test.testname + " modifiers mask matches"); + } + + var expectedState; + switch (event.type) { + case "popupshowing": + expectedState = "showing"; + break; + case "popupshown": + expectedState = "open"; + break; + case "popuphiding": + expectedState = "hiding"; + break; + case "popuphidden": + expectedState = "closed"; + break; + } + + if (gExpectedTriggerNode && event.type == "popupshowing") { + if (gExpectedTriggerNode == "notset") { + // check against null instead + gExpectedTriggerNode = null; + } + + is( + event.originalTarget.triggerNode, + gExpectedTriggerNode, + test.testname + " popupshowing triggerNode" + ); + } + + if (expectedState) { + is( + event.originalTarget.state, + expectedState, + test.testname + " " + event.type + " state" + ); + } + + if (matches) { + gTestEventIndex++; + if (events.length <= gTestEventIndex) { + setTimeout(checkResult, 0); + } + } else { + info(`Actual events so far: ${JSON.stringify(gActualEvents)}`); + } + } +} + +async function checkResult() { + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) { + step = test.steps[gTestStepIndex]; + } + + if ("result" in test) { + await test.result(test.testname, step); + } + + if ("autohide" in test) { + gAutoHide = true; + document.getElementById(test.autohide).hidePopup(); + return; + } + + goNextStep(); +} + +function goNextStep() { + info(`events: ${JSON.stringify(gActualEvents)}`); + gTestEventIndex = 0; + gActualEvents = []; + + var step = null; + var test = gPopupTests[gTestIndex]; + if ("steps" in test) { + gTestStepIndex++; + step = test.steps[gTestStepIndex]; + if (gTestStepIndex < test.steps.length) { + test.test(test.testname, step); + return; + } + } + + goNext(); +} + +function goNext() { + // We want to continue after the next animation frame so that + // we're in a stable state and don't get spurious mouse events at unexpected targets. + window.requestAnimationFrame(function () { + setTimeout(goNextStepSync, 0); + }); +} + +function goNextStepSync() { + if ( + gTestIndex >= 0 && + "end" in gPopupTests[gTestIndex] && + gPopupTests[gTestIndex].end + ) { + finish(); + return; + } + + gTestIndex++; + gTestStepIndex = 0; + if (gTestIndex < gPopupTests.length) { + var test = gPopupTests[gTestIndex]; + // Set the location hash so it's easy to see which test is running + document.location.hash = test.testname; + info("Starting " + test.testname); + + // skip the test if the condition returns false + if ("condition" in test && !test.condition()) { + goNext(); + return; + } + + // start with the first step if there are any + var step = null; + if ("steps" in test) { + step = test.steps[gTestStepIndex]; + } + + test.test(test.testname, step); + + // no events to check for so just check the result + if (!("events" in test)) { + checkResult(); + } else if (typeof test.events == "function" && !test.events().length) { + checkResult(); + } + } else { + finish(); + } +} + +function openMenu(menu) { + if ("open" in menu) { + menu.open = true; + } else if (menu.hasMenu()) { + menu.openMenu(true); + } else { + synthesizeMouse(menu, 4, 4, {}); + } +} + +function closeMenu(menu, popup) { + if ("open" in menu) { + menu.open = false; + } else if (menu.hasMenu()) { + menu.openMenu(false); + } else { + popup.hidePopup(); + } +} + +function checkActive(popup, id, testname) { + var activeok = true; + var children = popup.childNodes; + for (var c = 0; c < children.length; c++) { + var child = children[c]; + if ( + (id == child.id && child.getAttribute(menuactiveAttribute) != "true") || + (id != child.id && child.hasAttribute(menuactiveAttribute) != "") + ) { + activeok = false; + break; + } + } + ok(activeok, testname + " item " + (id ? id : "none") + " active"); +} + +function checkOpen(menuid, testname) { + var menu = document.getElementById(menuid); + if ("open" in menu) { + ok(menu.open, testname + " " + menuid + " menu is open"); + } else if (menu.hasMenu()) { + ok( + menu.getAttribute("open") == "true", + testname + " " + menuid + " menu is open" + ); + } +} + +function checkClosed(menuid, testname) { + var menu = document.getElementById(menuid); + if ("open" in menu) { + ok(!menu.open, testname + " " + menuid + " menu is open"); + } else if (menu.hasMenu()) { + ok(!menu.hasAttribute("open"), testname + " " + menuid + " menu is closed"); + } +} + +function convertPosition(anchor, align) { + if (anchor == "topleft" && align == "topleft") { + return "overlap"; + } + if (anchor == "topleft" && align == "topright") { + return "start_before"; + } + if (anchor == "topleft" && align == "bottomleft") { + return "before_start"; + } + if (anchor == "topright" && align == "topleft") { + return "end_before"; + } + if (anchor == "topright" && align == "bottomright") { + return "before_end"; + } + if (anchor == "bottomleft" && align == "bottomright") { + return "start_after"; + } + if (anchor == "bottomleft" && align == "topleft") { + return "after_start"; + } + if (anchor == "bottomright" && align == "bottomleft") { + return "end_after"; + } + if (anchor == "bottomright" && align == "topright") { + return "after_end"; + } + return ""; +} + +/* + * When checking position of the bottom or right edge of the popup's rect, + * use this instead of strict equality check of rounded values, + * because we snap the top/left edges to pixel boundaries, + * which can shift the bottom/right up to 0.5px from its "ideal" location, + * and could cause it to round differently. (See bug 622507.) + */ +function isWithinHalfPixel(a, b, message) { + ok(Math.abs(a - b) <= 0.5, `${message}: ${a}, ${b}`); +} + +function compareEdge(anchor, popup, edge, offsetX, offsetY, testname) { + testname += " " + edge; + + checkOpen(anchor.id, testname); + + var anchorrect = anchor.getBoundingClientRect(); + var popuprect = popup.getBoundingClientRect(); + + if (gPopupWidth == -1) { + ok( + Math.round(popuprect.right) - Math.round(popuprect.left) && + Math.round(popuprect.bottom) - Math.round(popuprect.top), + testname + " size" + ); + } else { + is(Math.round(popuprect.width), gPopupWidth, testname + " width"); + is(Math.round(popuprect.height), gPopupHeight, testname + " height"); + } + + var spaceIdx = edge.indexOf(" "); + if (spaceIdx > 0) { + let cornerX, cornerY; + let [position, align] = edge.split(" "); + switch (position) { + case "topleft": + cornerX = anchorrect.left; + cornerY = anchorrect.top; + break; + case "topcenter": + cornerX = anchorrect.left + anchorrect.width / 2; + cornerY = anchorrect.top; + break; + case "topright": + cornerX = anchorrect.right; + cornerY = anchorrect.top; + break; + case "leftcenter": + cornerX = anchorrect.left; + cornerY = anchorrect.top + anchorrect.height / 2; + break; + case "rightcenter": + cornerX = anchorrect.right; + cornerY = anchorrect.top + anchorrect.height / 2; + break; + case "bottomleft": + cornerX = anchorrect.left; + cornerY = anchorrect.bottom; + break; + case "bottomcenter": + cornerX = anchorrect.left + anchorrect.width / 2; + cornerY = anchorrect.bottom; + break; + case "bottomright": + cornerX = anchorrect.right; + cornerY = anchorrect.bottom; + break; + } + + switch (align) { + case "topleft": + cornerX += offsetX; + cornerY += offsetY; + break; + case "topcenter": + cornerX += -popuprect.width / 2 + offsetX; + cornerY += offsetY; + break; + case "topright": + cornerX += -popuprect.width + offsetX; + cornerY += offsetY; + break; + case "leftcenter": + cornerX += offsetX; + cornerY += -popuprect.height / 2 + offsetY; + break; + case "rightcenter": + cornerX += -popuprect.width + offsetX; + cornerY += -popuprect.height / 2 + offsetY; + break; + case "bottomleft": + cornerX += offsetX; + cornerY += -popuprect.height + offsetY; + break; + case "bottomcenter": + cornerX += -popuprect.width / 2 + offsetX; + cornerY += -popuprect.height + offsetY; + break; + case "bottomright": + cornerX += -popuprect.width + offsetX; + cornerY += -popuprect.height + offsetY; + break; + } + + is( + Math.round(popuprect.left), + Math.round(cornerX), + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(cornerY), + testname + " y position" + ); + return; + } + + if (edge == "after_pointer") { + is( + Math.round(popuprect.left), + Math.round(anchorrect.left) + offsetX, + testname + " x position" + ); + is( + Math.round(popuprect.top), + Math.round(anchorrect.top) + offsetY + 21, + testname + " y position" + ); + return; + } + + if (edge == "overlap") { + is( + Math.round(anchorrect.left) + offsetY, + Math.round(popuprect.left), + testname + " position1" + ); + is( + Math.round(anchorrect.top) + offsetY, + Math.round(popuprect.top), + testname + " position2" + ); + return; + } + + if (edge.indexOf("before") == 0) { + isWithinHalfPixel( + anchorrect.top + offsetY, + popuprect.bottom, + testname + " position1" + ); + } else if (edge.indexOf("after") == 0) { + is( + Math.round(anchorrect.bottom) + offsetY, + Math.round(popuprect.top), + testname + " position1" + ); + } else if (edge.indexOf("start") == 0) { + isWithinHalfPixel( + anchorrect.left + offsetX, + popuprect.right, + testname + " position1" + ); + } else if (edge.indexOf("end") == 0) { + is( + Math.round(anchorrect.right) + offsetX, + Math.round(popuprect.left), + testname + " position1" + ); + } + + if (0 < edge.indexOf("before")) { + is( + Math.round(anchorrect.top) + offsetY, + Math.round(popuprect.top), + testname + " position2" + ); + } else if (0 < edge.indexOf("after")) { + isWithinHalfPixel( + anchorrect.bottom + offsetY, + popuprect.bottom, + testname + " position2" + ); + } else if (0 < edge.indexOf("start")) { + is( + Math.round(anchorrect.left) + offsetX, + Math.round(popuprect.left), + testname + " position2" + ); + } else if (0 < edge.indexOf("end")) { + isWithinHalfPixel( + anchorrect.right + offsetX, + popuprect.right, + testname + " position2" + ); + } +} diff --git a/toolkit/content/tests/widgets/seek_with_sound.ogg b/toolkit/content/tests/widgets/seek_with_sound.ogg Binary files differnew file mode 100644 index 0000000000..c86d9946bd --- /dev/null +++ b/toolkit/content/tests/widgets/seek_with_sound.ogg diff --git a/toolkit/content/tests/widgets/test-webvtt-1.vtt b/toolkit/content/tests/widgets/test-webvtt-1.vtt new file mode 100644 index 0000000000..20c9fc94cc --- /dev/null +++ b/toolkit/content/tests/widgets/test-webvtt-1.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:02.000 +This is a one line text track + +2 +00:00:05.000 --> 00:00:07.000 +- <b>This is a new text track but bolded</b> +- <i>This line should be italicized<i> diff --git a/toolkit/content/tests/widgets/test-webvtt-2.vtt b/toolkit/content/tests/widgets/test-webvtt-2.vtt new file mode 100644 index 0000000000..48dd63a9ee --- /dev/null +++ b/toolkit/content/tests/widgets/test-webvtt-2.vtt @@ -0,0 +1,10 @@ +WEBVTT + +1 +00:00:00.000 --> 00:00:02.000 +Voici un text track en une ligne + +2 +00:00:05.000 --> 00:00:07.000 +- <b>Voici un nouveau text track en gras</b> +- <i>Cette ligne devrait être en italiques<i> diff --git a/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html new file mode 100644 index 0000000000..2d29fe9be4 --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_dimensions.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + const audio = document.getElementById("audio"); + const controlBar = getElementWithinVideo(audio, "controlBar"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + audio.addEventListener("loadedmetadata", resolve, {once: true}); + audio.src = "audio.wav"; + }); + }); + + add_task(async function check_audio_height() { + is(audio.clientHeight, 40, "checking height of audio element"); + }); + + add_task(async function check_controlbar_width() { + const originalControlBarWidth = controlBar.clientWidth; + + isnot(originalControlBarWidth, 400, "the default audio width is not 400px"); + + audio.style.width = "400px"; + audio.offsetWidth; // force reflow + + isnot(controlBar.clientWidth, originalControlBarWidth, "new width should differ from the origianl width"); + is(controlBar.clientWidth, 400, "controlbar's width should grow with audio width"); + }); + + add_task(function check_audio_height_construction_sync() { + let el = new Audio(); + el.src = "audio.wav"; + el.controls = true; + document.body.appendChild(el); + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); + + add_task(function check_audio_height_add_control_sync() { + let el = new Audio(); + el.src = "audio.wav"; + document.body.appendChild(el); + is(el.clientHeight, 0, "Height of audio element without controls"); + el.controls = true; + is(el.clientHeight, 40, "Height of audio element with controls"); + document.body.removeChild(el); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html b/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html new file mode 100644 index 0000000000..6963c3da1e --- /dev/null +++ b/toolkit/content/tests/widgets/test_audiocontrols_fullscreen.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Audio controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls preload="auto"></audio> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + const audio = document.getElementById("audio"); + const controlBar = getElementWithinVideo(audio, "controlBar"); + + add_setup(async function setup() { + await new Promise(resolve => { + audio.addEventListener("loadedmetadata", resolve, {once: true}); + audio.src = "audio.wav"; + }); + }); + + add_task(async function test_double_click_does_not_fullscreen() { + SimpleTest.requestCompleteLog(); + SimpleTest.requestFlakyTimeout("Waiting to ensure that fullscreen event does not fire"); + const { x, y } = audio.getBoundingClientRect(); + const endedPromise = new Promise(resolve => { + audio.addEventListener("ended", () => { + info('Audio ended event fired!'); + resolve(); + }, { once: true }); + setTimeout( () => { + info('Audio ran out of time before ended event fired!'); + resolve(); + }, audio.duration * 1000); + }); + let noFullscreenEvent = true; + document.addEventListener("mozfullscreenchange", () => { + noFullscreenEvent = false; + }, { once: true }); + info("Simulate double click on media player."); + synthesizeMouse(audio, x, y, { clickCount: 2 }); + info("Waiting for video to end..."); + await endedPromise; + // By this point, if the double click was going to trigger fullscreen then + // it should have happened by now. + ok( + noFullscreenEvent, + "Double clicking should not trigger fullscreen event" + ); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_bug1654500.html b/toolkit/content/tests/widgets/test_bug1654500.html new file mode 100644 index 0000000000..9bb05ba9c8 --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug1654500.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Clear disabled/readonly datetime inputs</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<p>Disabled inputs should be able to be cleared just as they can be set with real values</p> +<input type="date" id="date1" value="2020-08-11" disabled> +<input type="date" id="date2" value="2020-08-11" readonly> +<input type="time" id="time1" value="07:01" disabled> +<input type="time" id="time2" value="07:01" readonly> +<script> +/* global date1, date2, time1, time2 */ +function querySelectorAllShadow(parent, selector) { + const shadowRoot = SpecialPowers.wrap(parent).openOrClosedShadowRoot; + return shadowRoot.querySelectorAll(selector); +} +date1.value = date2.value = ""; +time1.value = time2.value = ""; +for (const date of [date1, date2]) { + const fields = [...querySelectorAllShadow(date, ".datetime-edit-field")]; + is(fields.length, 3, "Three numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +for (const time of [time1, time2]) { + const fields = [...querySelectorAllShadow(time, ".datetime-edit-field")]; + ok(fields.length >= 2, "At least two numeric fields are expected"); + for (const field of fields) { + is(field.getAttribute("value"), "", "All fields should be cleared"); + } +} +</script> diff --git a/toolkit/content/tests/widgets/test_bug898940.html b/toolkit/content/tests/widgets/test_bug898940.html new file mode 100644 index 0000000000..f2349c9a4c --- /dev/null +++ b/toolkit/content/tests/widgets/test_bug898940.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an audio element that's already playing when controls are attached displays the controls</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <audio id="audio" controls src="audio.ogg"></audio> +</div> + +<pre id="test"> +<script class="testbody"> + var audio = document.getElementById("audio"); + audio.play(); + audio.ontimeupdate = function doTest() { + ok(audio.getBoundingClientRect().height > 0, + "checking audio element height is greater than zero"); + audio.ontimeupdate = null; + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml new file mode 100644 index 0000000000..88511001b7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_menugroup.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Context menugroup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="context"> + <menugroup> + <menuitem id="a"/> + <menuitem id="b"/> + </menugroup> + <menuitem id="c" label="c"/> + <menugroup/> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="context" style="width: 20px; height: 20px"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var gMenuPopup = $("context"); +ok(gMenuPopup, "Got the reference to the context menu"); + +var popupTests = [ +{ + testname: "one-down-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing context", "popupshown context", "DOMMenuItemActive a" ], + test() { + synthesizeMouse($("popuparea"), 4, 4, {}); + synthesizeKey("KEY_ArrowDown"); + }, + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "two-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowDown"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +{ + testname: "three-down-keys-one-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive c", "DOMMenuItemActive b" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result (testname) { + checkActive(gMenuPopup, "b", testname); + } +}, +{ + testname: "three-down-keys-two-up-keys", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive b", "DOMMenuItemActive a" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "a", testname); + } +}, +{ + testname: "three-down-keys-three-up-key", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemInactive a", "DOMMenuItemActive c" ], + test: () => synthesizeKey("KEY_ArrowUp"), + result(testname) { + checkActive(gMenuPopup, "c", testname); + } +}, +]; + +SimpleTest.waitForFocus(function runTest() { + startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml new file mode 100644 index 0000000000..883e8e4d98 --- /dev/null +++ b/toolkit/content/tests/widgets/test_contextmenu_nested.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Nested Context Menu Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<menupopup id="outercontext"> + <menuitem label="Context One"/> + <menu id="outercontextmenu" label="Sub"> + <menupopup id="innercontext"> + <menuitem id="innercontextmenu" label="Sub Context One"/> + </menupopup> + </menu> +</menupopup> + +<menupopup id="outermain"> + <menuitem label="One"/> + <menu id="outermenu" label="Sub"> + <menupopup id="innermain"> + <menuitem id="innermenu" label="Sub One" context="outercontext"/> + </menupopup> + </menu> +</menupopup> + +<button label="Check"/> + +<vbox id="popuparea" popup="outermain" style="width: 20px; height: 20px"/> + +<script type="application/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var popupTests = [ +{ + testname: "open outer popup", + events: [ "popupshowing outermain", "popupshown outermain" ], + test: () => synthesizeMouse($("popuparea"), 4, 4, {}), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname); + } +}, +{ + testname: "open inner popup", + events: [ "DOMMenuItemActive outermenu", "popupshowing innermain", "popupshown innermain" ], + test () { + synthesizeMouse($("outermenu"), 4, 4, { type: "mousemove" }); + synthesizeMouse($("outermenu"), 2, 2, { type: "mousemove" }); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + } +}, +{ + testname: "open outer context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popupshowing outercontext", "popupshown outercontext" ], + test: () => synthesizeMouse($("innermenu"), 4, 4, { type: "contextmenu", button: 2 }), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + } +}, +{ + testname: "open inner context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "DOMMenuItemActive outercontextmenu", "popupshowing innercontext", "popupshown innercontext" ], + test () { + synthesizeMouse($("outercontextmenu"), 4, 4, { type: "mousemove" }); + setTimeout(function() { + synthesizeMouse($("outercontextmenu"), 2, 2, { type: "mousemove" }); + }, 1000); + }, + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, $("innermenu"), testname + " outer context"); + is($("innercontext").triggerNode, $("innermenu"), testname + " inner context"); + } +}, +{ + testname: "close context", + condition() { return (!navigator.platform.includes("Mac")); }, + events: [ "popuphiding innercontext", "popuphidden innercontext", + "popuphiding outercontext", "popuphidden outercontext", + "DOMMenuInactive innercontext", + "DOMMenuItemInactive outercontextmenu", + "DOMMenuInactive outercontext" ], + test: () => $("outercontext").hidePopup(), + result (testname) { + is($("outermain").triggerNode, $("popuparea"), testname + " outer"); + is($("innermain").triggerNode, $("popuparea"), testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + } +}, +{ + testname: "hide menus", + events: [ "popuphiding innermain", "popuphidden innermain", + "popuphiding outermain", "popuphidden outermain", + "DOMMenuInactive innermain", + "DOMMenuItemInactive outermenu", + "DOMMenuInactive outermain" ], + + test: () => $("outermain").hidePopup(), + result (testname) { + is($("outermain").triggerNode, null, testname + " outer"); + is($("innermain").triggerNode, null, testname + " inner"); + is($("outercontext").triggerNode, null, testname + " outer context"); + is($("innercontext").triggerNode, null, testname + " inner context"); + } +} +]; + +SimpleTest.waitForFocus(function runTest() { + return startPopupTests(popupTests); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"><p id="display"/></body> + +</window> diff --git a/toolkit/content/tests/widgets/test_editor_currentURI.xhtml b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml new file mode 100644 index 0000000000..bbb1f33623 --- /dev/null +++ b/toolkit/content/tests/widgets/test_editor_currentURI.xhtml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Editor currentURI Tests" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="html" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var editor = document.getElementById("editor"); + // Check that currentURI is a property of editor. + var result = "currentURI" in editor; + is(result, true, "currentURI is a property of editor"); + is(editor.currentURI.spec, "about:blank", "currentURI.spec is about:blank"); + SimpleTest.finish(); + } +]]> +</script> +</window> diff --git a/toolkit/content/tests/widgets/test_image_recognition.html b/toolkit/content/tests/widgets/test_image_recognition.html new file mode 100644 index 0000000000..d4dfc6216e --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + const { TestUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" + ); + + function pushPref(preferenceName, value) { + return new Promise(resolve => { + const options = {"set": [[preferenceName, value]]}; + SpecialPowers.pushPrefEnv(options, resolve); + }); + } + + add_task(async () => { + // Performing text recognition in CI can take some time, and test verify runs have + // timed out. + SimpleTest.requestLongerTimeout(2); + + await pushPref("dom.text-recognition.shadow-dom-enabled", true); + const img = document.querySelector("#content img"); + + info("Recognizing the image text"); + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + is(result.length, 2, "Two words were found."); + const mozilla = result.find(r => r.string === "Mozilla"); + const firefox = result.find(r => r.string === "Firefox"); + + ok(mozilla, "The word Mozilla was found."); + ok(firefox, "The word Firefox was found."); + + ok(mozilla.quad.p1.x < firefox.quad.p2.x, "The Mozilla text is left of Firefox"); + ok(mozilla.quad.p1.y > firefox.quad.p2.y, "The Mozilla text is above Firefox"); + + const spans = await TestUtils.waitForCondition( + () => shadowRootQuerySelectorAll(img, "span"), + "Attempting to get image recognition spans." + ); + + const mozillaSpan = [...spans].find(s => s.innerText === "Mozilla"); + const firefoxSpan = [...spans].find(s => s.innerText === "Firefox"); + + ok(mozillaSpan, "The word Mozilla span was found."); + ok(firefoxSpan, "The word Firefox span was found."); + + ok(mozillaSpan.style.transform.startsWith("matrix3d("), "A matrix transform was applied"); + ok(firefoxSpan.style.transform.startsWith("matrix3d("), "A matrix transform was applied"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_image_recognition_unsupported.html b/toolkit/content/tests/widgets/test_image_recognition_unsupported.html new file mode 100644 index 0000000000..f67ea8eb00 --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition_unsupported.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition unsupported</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + /** + * This test is for platforms that do not support text recognition. + */ + add_task(async () => { + const img = document.querySelector("#content img"); + + info("Recognizing the current image text is not supported on this platform."); + try { + await SpecialPowers.wrap(img).recognizeCurrentImageText(); + ok(false, "Recognizing the text should not be supported."); + } catch (error) { + ok(error, "Expected unsupported message: " + error.message); + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_image_recognition_zh.html b/toolkit/content/tests/widgets/test_image_recognition_zh.html new file mode 100644 index 0000000000..ba8f3c94e1 --- /dev/null +++ b/toolkit/content/tests/widgets/test_image_recognition_zh.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Image recognition test for Chinese</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <img src="image-zh.png" /> +</div> + +<pre id="test"> +<script class="testbody"> + // Performing text recognition in CI can take some time, and test verify runs have + // timed out. + SimpleTest.requestLongerTimeout(2); + + /** + * This test exercises the code path where the image recognition APIs detect the + * document language and use it to choose the language. + */ + add_task(async () => { + const img = document.querySelector("#content img"); + + info("Recognizing the image text, but not as Chinese"); + { + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + for (const { string } of result) { + isnot(string, "火狐", 'The results are (as expected) incorrect, as Chinese was not set as the language.'); + } + } + + info("Setting the document to Chinese."); + document.documentElement.setAttribute("lang", "zh-Hans-CN"); + + info("Recognizing the image text"); + { + const result = await SpecialPowers.wrap(img).recognizeCurrentImageText(); + is(result.length, 1, "One word was found."); + is(result[0].string, "火狐", "The Chinese characters for Firefox are found."); + } + + document.documentElement.setAttribute("lang", "en-US"); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_label_checkbox.xhtml b/toolkit/content/tests/widgets/test_label_checkbox.xhtml new file mode 100644 index 0000000000..da9d588409 --- /dev/null +++ b/toolkit/content/tests/widgets/test_label_checkbox.xhtml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Label Checkbox Tests" + onload="onLoad()" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Label Checkbox Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + runTest(); +} + +function runTest() +{ + window.open("window_label_checkbox.xhtml", "_blank", "width=600,height=600"); +} + +onmessage = function onMessage() +{ + SimpleTest.finish(); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_menubar.xhtml b/toolkit/content/tests/widgets/test_menubar.xhtml new file mode 100644 index 0000000000..16fae52b03 --- /dev/null +++ b/toolkit/content/tests/widgets/test_menubar.xhtml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Menubar Popup Tests" + onload="setTimeout(onLoad, 0);" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <title>Menubar Popup Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +SimpleTest.waitForExplicitFinish(); +function onLoad() +{ + window.open("window_menubar.xhtml", "_blank", "width=600,height=600"); +} +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_mousecapture_area.html b/toolkit/content/tests/widgets/test_mousecapture_area.html new file mode 100644 index 0000000000..7217d971eb --- /dev/null +++ b/toolkit/content/tests/widgets/test_mousecapture_area.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Mouse capture on area elements tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <!-- The border="0" on the images is needed so that when we use + synthesizeMouse we don't accidentally target the border of the image and + miss the area because synthesizeMouse gets the rect of the primary frame + of the target (the area), which is the image due to bug 135040, which + includes the border, but the events targetted at the border aren't + targeted at the area. --> + + <!-- 20x20 of red --> + <img id="image" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#Map"/> + + <map name="Map"> + <!-- area over the whole image --> + <area id="area" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <!-- 20x20 of red --> + <img id="img1" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <!-- 20x20 of red --> + <img id="img2" border="0" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + usemap="#sharedMap"/> + + <map name="sharedMap"> + <!-- area over the whole image --> + <area id="sharedarea" onmousedown="this.setCapture();" onmouseup="this.releaseCapture();" + shape="poly" coords="0,0, 0,20, 20,20, 20,0" href="javascript:void(0);"/> + </map> + + + <div id="otherelement" style="width: 100px; height: 100px;"></div> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + // XXX We send a useless click to each image to force it to setup its image + // map, because flushing layout won't do it. Hopefully bug 135040 will make + // this not suck. + synthesizeMouse($("image"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("image"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img1"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img1"), 5, 5, { type: "mouseup" }); + synthesizeMouse($("img2"), 5, 5, { type: "mousedown" }); + synthesizeMouse($("img2"), 5, 5, { type: "mouseup" }); + + + // test that setCapture works on an area element (bug 517737) + var area = document.getElementById("area"); + synthesizeMouse(area, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + area, "mousemove", "setCapture works on areas"); + synthesizeMouse(area, 5, 5, { type: "mouseup" }); + + // test that setCapture works on an area element when it is part of an image + // map that is used by two images + + var img1 = document.getElementById("img1"); + var sharedarea = document.getElementById("sharedarea"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img1, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img1, 5, 5, { type: "mouseup" }); + + var img2 = document.getElementById("img2"); + // synthesizeMouse just sends the event by coordinates, so this is really a click on the area + synthesizeMouse(img2, 5, 5, { type: "mousedown" }); + synthesizeMouseExpectEvent($("otherelement"), 5, 5, { type: "mousemove" }, + sharedarea, "mousemove", "setCapture works on areas with multiple images"); + synthesizeMouse(img2, 5, 5, { type: "mouseup" }); + + // Bug 862673 - nuke all content so assertions in this test are attributed to + // this test rather than the one which happens to follow. + var content = document.getElementById("content"); + content.remove(); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_button_group.html b/toolkit/content/tests/widgets/test_moz_button_group.html new file mode 100644 index 0000000000..92fd7776fa --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_button_group.html @@ -0,0 +1,235 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>moz-button-group tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-button-group.mjs"></script> + </head> + <body> + <p id="display"></p> + <div id="content"> + <button id="before-button">Before</button> + <div id="render"></div> + <button id="after-button">After</button> + </div> + <!-- This is here to ensure the stylesheet is loaded. It gets removed in setup. --> + <moz-button-group></moz-button-group> + <pre id="test"> + </pre> + + <script> + let html; + let render; + + let renderArea = document.getElementById("render"); + let beforeButton = document.getElementById("before-button"); + let afterButton = document.getElementById("after-button"); + + async function checkButtons(...buttons) { + const checkOrder = (a, b) => { + let firstBounds = a.getBoundingClientRect(); + let secondBounds = b.getBoundingClientRect(); + + ok(firstBounds.right < secondBounds.left, `First button comes first`); + let locationDiff = Math.abs(secondBounds.left - firstBounds.right - 8); + ok(locationDiff < 1, `Second button is 8px after first (${locationDiff}, ${firstBounds.right}, ${secondBounds.left})`); + }; + + ok(buttons.length >= 2, "There are at least 2 buttons to check"); + + // Verify tab order is correct. + beforeButton.focus(); + is(document.activeElement, beforeButton, "Before button is focused"); + + synthesizeKey("VK_TAB"); + is(document.activeElement, buttons[0], "Next button is focused"); + + for (let i = 1; i < buttons.length; i++) { + // Confirm button order in DOM + checkOrder(buttons[i - 1], buttons[i]); + + synthesizeKey("VK_TAB"); + is(document.activeElement, buttons[i], "Next button is focused"); + } + + synthesizeKey("VK_TAB"); + is(document.activeElement, afterButton, "After button is at the end in tab order"); + + // Verify light DOM order is correct, in case of manual tab management in JS. + let { parentElement } = buttons[0]; + let parentChildren = parentElement.children; + is(parentChildren.length, buttons.length, "Expected number of children"); + for (let i = 0; i < parentChildren.length; i++) { + is(parentChildren[i], buttons[i], `Button ${i} is in correct light DOM spot`); + } + } + + + add_setup(async function setup() { + ({ html, render} = await import("chrome://global/content/vendor/lit.all.mjs")); + document.querySelector("moz-button-group").remove(); + }); + + add_task(async function testButtonOrderingSlot() { + render( + html` + <moz-button-group> + <button slot="primary" id="primary-button">Primary</button> + <button id="secondary-button">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let primaryButton = document.getElementById("primary-button"); + let secondaryButton = document.getElementById("secondary-button"); + + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + await checkButtons(primaryButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, primaryButton); + }); + + add_task(async function testPrimaryButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button class="primary">Primary</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let primaryButton = buttonGroup.querySelector(".primary"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(primaryButton.slot, "primary", "primary button was auto-slotted") + await checkButtons(primaryButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, primaryButton); + }); + + add_task(async function testSubmitButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button type="submit">Submit</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let submitButton = buttonGroup.querySelector("[type=submit]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(submitButton.slot, "primary", "submit button was auto-slotted") + await checkButtons(submitButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, submitButton); + }); + + add_task(async function testAutofocusButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button autofocus>First</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let autofocusButton = buttonGroup.querySelector("[autofocus]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(autofocusButton.slot, "primary", "autofocus button was auto-slotted") + await checkButtons(autofocusButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, autofocusButton); + }); + + add_task(async function testDefaultButtonAutoSlotting() { + render( + html` + <moz-button-group> + <button default>First</button> + <button class="secondary">Secondary</button> + </moz-button-group> + `, + renderArea + ); + + let buttonGroup = document.querySelector("moz-button-group"); + let defaultButton = buttonGroup.querySelector("[default]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + buttonGroup.platform = "win"; + await buttonGroup.updateComplete; + is(defaultButton.slot, "primary", "default button was auto-slotted") + await checkButtons(defaultButton, secondaryButton); + + buttonGroup.platform = "macosx"; + await buttonGroup.updateComplete; + await checkButtons(secondaryButton, defaultButton); + }); + + add_task(async function testInitialButtonLightDomReordering() { + const renderPlatform = platform => render( + html` + <moz-button-group .platform=${platform}> + <button class="primary">First</button> + <button class="secondary">Secondary</button> + <button default>Default</button> + </moz-button-group> + `, + renderArea + ); + + renderPlatform("win"); + let buttonGroup = document.querySelector("moz-button-group"); + await buttonGroup.updateComplete; + let primaryButton = buttonGroup.querySelector(".primary"); + let defaultButton = buttonGroup.querySelector("[default]"); + let secondaryButton = buttonGroup.querySelector(".secondary"); + + is(primaryButton.slot, "primary", "primary button was auto-slotted"); + is(defaultButton.slot, "primary", "default button was auto-slotted"); + await checkButtons(primaryButton, defaultButton, secondaryButton); + + renderPlatform("macosx"); + buttonGroup = document.querySelector("moz-button-group"); + await buttonGroup.updateComplete; + primaryButton = buttonGroup.querySelector(".primary"); + defaultButton = buttonGroup.querySelector("[default]"); + secondaryButton = buttonGroup.querySelector(".secondary"); + + is(primaryButton.slot, "primary", "primary button was auto-slotted"); + is(defaultButton.slot, "primary", "default button was auto-slotted"); + await checkButtons(secondaryButton, primaryButton, defaultButton); + }); + </script> + </body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_card.html b/toolkit/content/tests/widgets/test_moz_card.html new file mode 100644 index 0000000000..ef4e67d0fa --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_card.html @@ -0,0 +1,158 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>moz-card tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script type="module" src="chrome://global/content/elements/moz-card.mjs"></script> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" /> +</head> + +<body> + <p id="display"></p> + <div id="content"> + <moz-card id="default-card" data-l10n-id="test-id-1" data-l10n-attrs="heading"> + <div>TEST</div> + </moz-card> + <hr /> + + <moz-card id="accordion-card" data-l10n-id="test-id-2" data-l10n-attrs="heading" heading="accordion heading" + type="accordion"> + <div>accordion test content</div> + </moz-card> + <hr /> + + </div> + <pre id="test"></pre> + <script> + let generatedSlotText = "generated slotted element"; + let testHeading = "test heading"; + + function assertBasicProperties(card, expectedValues) { + info(`Testing card with ID: ${card.id}`); + ok(card, "The card element should exist"); + is(card.localName, "moz-card", "The card should have the correct tag"); + let l10nId = card.getAttribute("data-l10n-id"); + let l10nAttrs = card.getAttribute("data-l10n-attrs"); + if (expectedValues["data-l10n-id"]) { + is(l10nId, expectedValues["data-l10n-id"], "l10n id should be unchanged"); + } + if (expectedValues["data-l10n-attrs"]) { + is(l10nAttrs, expectedValues["data-l10n-attrs"], "l10n attrs should be unchanged"); + } + let cardContent = card.firstElementChild; + ok(cardContent, "The content should exist"); + is(cardContent.textContent, expectedValues.contentText, "The content should be unchanged"); + is(card.contentSlotEl.id, "content", "The content container should have the correct ID"); + if (card.type != "accordion") { + is(card.contentSlotEl.getAttribute("aria-describedby"), "content", "The content container should be described by the 'content' slot"); + } + + if (expectedValues.headingText) { + ok(card.headingEl, "Heading should exist"); + is(card.headingEl.textContent, expectedValues.headingText, "Heading should match the 'heading' attribute value"); + } + + } + + function assertAccordionCardProperties(card, expectedValues) { + ok(card.detailsEl, "The details element should exist"); + ok(card.detailsEl.querySelector("summary"), "There should be a summary element within the details element"); + ok(card.detailsEl.querySelector("summary").querySelector(".chevron-icon"), "There should be a chevron icon div within the summary element"); + } + + async function generateCard(values) { + let card = document.createElement("moz-card"); + for (let [key, value] of Object.entries(values)) { + card.setAttribute(key, value); + } + let div = document.createElement("div"); + div.innerText = generatedSlotText; + card.appendChild(div); + document.body.appendChild(card); + await card.updateComplete; + document.body.appendChild(document.createElement("hr")); + return card; + } + + add_task(async function testDefaultCard() { + assertBasicProperties(document.getElementById("default-card"), + { + "data-l10n-id": "test-id-1", + "data-l10n-attrs": "heading", + contentText: "TEST" + } + ); + + let defaultCard = await generateCard( + { + "data-l10n-id": "generated-id-1", + "data-l10n-attrs": "heading", + heading: testHeading, + id: "generated-default-card" + } + ); + + assertBasicProperties(defaultCard, + { + "data-l10n-id": "generated-id-1", + "data-l10n-attrs": "heading", + contentText: generatedSlotText, + heading: testHeading + } + ); + }); + + add_task(async function testAccordionCard() { + assertBasicProperties(document.getElementById("accordion-card"), + { + "data-l10n-id": "test-id-2", + "data-l10n-attrs": "heading", + contentText: "accordion test content", + headingText: "accordion heading", + } + ); + assertAccordionCardProperties(document.getElementById("accordion-card"), + { + "data-l10n-id": "test-id-2", + "data-l10n-attrs": "heading", + contentText: "accordion test content", + headingText: "accordion heading", + } + ); + + let accordionCard = await generateCard( + { + type: "accordion", + id: "generated-accordion-card", + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + heading: testHeading + } + ); + + assertBasicProperties(accordionCard, + { + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + assertAccordionCardProperties(accordionCard, + { + "data-l10n-id": "generated-id-2", + "data-l10n-attrs": "heading", + headingText: testHeading, + contentText: generatedSlotText, + } + ); + }); + + </script> +</body> + +</html> diff --git a/toolkit/content/tests/widgets/test_moz_five_star.html b/toolkit/content/tests/widgets/test_moz_five_star.html new file mode 100644 index 0000000000..593028f2c9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_five_star.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozFiveStar Tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="module" src="chrome://global/content/elements/moz-five-star.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="max-width: fit-content"> + <moz-five-star label="Label" rating="2.5"></moz-five-star> +</div> +<pre id="test"> + <script class="testbody" type="application/javascript"> + const { BrowserTestUtils } = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + add_task(async function testMozFiveStar() { + const mozFiveStar = document.querySelector("moz-five-star"); + ok(mozFiveStar, "moz-five-star is rendered"); + + const stars = mozFiveStar.starEls; + ok(stars, "moz-five-star has stars"); + is(stars.length, 5, "moz-five-star stars count is 5"); + + const rating = mozFiveStar.rating; + ok(rating, "moz-five-star has a rating"); + is(rating, 2.5, "moz-five-star rating is 2.5"); + }); + + add_task(async function testMozFiveStarsDisplay() { + const mozFiveStar = document.querySelector("moz-five-star"); + ok(mozFiveStar, "moz-five-star is rendered"); + + async function testRating(rating, ratingRounded, expectation) { + mozFiveStar.rating = rating; + await mozFiveStar.updateComplete; + if (mozFiveStar.ownerDocument.hasPendingL10nMutations) { + await BrowserTestUtils.waitForEvent( + mozFiveStar.ownerDocument, + "L10nMutationsFinished" + ); + } + let starsString = Array.from(mozFiveStar.starEls) + .map(star => star.getAttribute("fill")) + .join(","); + is(starsString, expectation, `Rendering of rating ${rating}`); + + is( + mozFiveStar.starsWrapperEl.title, + `Rated ${ratingRounded} out of 5`, + "Rendered title must contain at most one fractional digit" + ); + + let isImage = mozFiveStar.starsWrapperEl.getAttribute("role") == "img" + || mozFiveStar.starsWrapperEl.getAttribute("role") == "image"; + + ok( + isImage, + "Rating element is an image for the title to be announced" + ); + } + + await testRating(0.0, "0", "empty,empty,empty,empty,empty"); + await testRating(0.249, "0.2", "empty,empty,empty,empty,empty"); + await testRating(0.25, "0.3", "half,empty,empty,empty,empty"); + await testRating(0.749, "0.7", "half,empty,empty,empty,empty"); + await testRating(0.99, "1", "full,empty,empty,empty,empty"); + await testRating(1.0, "1", "full,empty,empty,empty,empty"); + await testRating(2, "2", "full,full,empty,empty,empty"); + await testRating(3.0, "3", "full,full,full,empty,empty"); + await testRating(4.001, "4", "full,full,full,full,empty"); + await testRating(4.249, "4.2", "full,full,full,full,empty"); + await testRating(4.25, "4.3", "full,full,full,full,half"); + await testRating(4.749, "4.7", "full,full,full,full,half"); + await testRating(4.89, "4.9", "full,full,full,full,full"); + await testRating(5.0, "5", "full,full,full,full,full"); + }); + </script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_label.html b/toolkit/content/tests/widgets/test_moz_label.html new file mode 100644 index 0000000000..80a9600930 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_label.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozLabel tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-label.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <label is="moz-label" for="checkbox" accesskey="c">For the checkbox:</label> + <input type="checkbox" id="checkbox" /> + + <label is="moz-label" accesskey="n"> + For the nested checkbox: + <input type="checkbox" /> + </label> + + <label is="moz-label" for="radio" accesskey="r">For the radio:</label> + <input type="radio" id="radio" /> + + <label is="moz-label" accesskey="F"> + For the nested radio: + <input type="radio" /> + </label> + + <label is="moz-label" for="button" accesskey="b">For the button:</label> + <button id="button">Click me</button> + + <label is="moz-label" accesskey="u"> + For the nested button: + <button>Click me too</button> + </label> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + let labels = document.querySelectorAll("label[is='moz-label']"); + let isMac = navigator.platform.includes("Mac"); + + function performAccessKey(key) { + synthesizeKey( + key, + navigator.platform.includes("Mac") + ? { altKey: true, ctrlKey: true } + : { altKey: true, shiftKey: true } + ); + } + + // Accesskey underlining is disabled by default on Mac. + // Reload the window and wait for load to ensure pref is applied. + add_setup(async function setup() { + if (isMac && !SpecialPowers.getIntPref("ui.key.menuAccessKey")) { + await SpecialPowers.pushPrefEnv( + { set: [["ui.key.menuAccessKey", 1]] }, + async () => { + window.location.reload(); + await new Promise(resolve => { + addEventListener("load", resolve, { once: true }); + }); + } + ); + } + }); + + add_task(async function testAccesskeyUnderlined() { + labels.forEach(label => { + let accessKey = label.getAttribute("accesskey"); + let wrapper = label.querySelector(".accesskey"); + is(wrapper.textContent, accessKey, "The accesskey character is wrapped.") + + let textDecoration = getComputedStyle(wrapper)["text-decoration"] + ok(textDecoration.includes("underline"), "The accesskey character is underlined.") + }) + }); + + add_task(async function testAccesskeyFocus() { + labels.forEach(label => { + let accessKey = label.getAttribute("accesskey"); + // Find the labelled element via the "for" attr if there's an ID + // association, or select the lastElementChild for nested elements + let element = document.getElementById(label.getAttribute("for")) || label.lastElementChild; + + isnot(document.activeElement, element, "Focus is not on the associated element."); + + performAccessKey(accessKey); + + is(document.activeElement, element, "Focus moved to the associated element.") + }) + }); + + add_task(async function testAccesskeyChange() { + let label = labels[0]; + let nextAccesskey = "x"; + let originalAccesskey = label.getAttribute("accesskey"); + let getWrapper = () => label.querySelector(".accesskey"); + is(getWrapper().textContent, originalAccesskey, "Original accesskey character is wrapped.") + + label.setAttribute("accesskey", nextAccesskey); + is(getWrapper().textContent, nextAccesskey, "New accesskey character is wrapped.") + + let elementId = label.getAttribute("for"); + let focusedEl = document.getElementById(elementId); + + performAccessKey(originalAccesskey); + isnot(document.activeElement.id, focusedEl.id, "Focus has not moved to the associated element.") + + performAccessKey(nextAccesskey); + is(document.activeElement.id, focusedEl.id, "Focus moved to the associated element.") + }); + + add_task(async function testAccesskeyAppended() { + let label = labels[0]; + let originalText = label.textContent; + let accesskey = "z"; // Letter not included in the label text. + label.setAttribute("accesskey", accesskey); + + let expectedText = `${originalText} (Z):`; + is(label.textContent, expectedText, "Access key is appended when not included in label text.") + }); + + add_task(async function testLabelClick() { + let label = labels[0]; + let input = document.getElementById(label.getAttribute("for")); + is(input.checked, false, "The associated input is not checked.") + + // Input state changes on label click. + synthesizeMouseAtCenter(label, {}); + ok(input.checked, "The associated input is checked.") + + // Input state doesn't change on label click when input is disabled. + input.disabled = true; + synthesizeMouseAtCenter(label, {}); + ok(input.checked, "The associated input is still checked.") + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_message_bar.html b/toolkit/content/tests/widgets/test_moz_message_bar.html new file mode 100644 index 0000000000..7ee6825ef3 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_message_bar.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozMessageBar tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="module" src="chrome://global/content/elements/moz-message-bar.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <moz-message-bar id="infoMessage" heading="Heading" message="Test message"></moz-message-bar> + <moz-message-bar id="infoMessage2" dismissable message="Test message"></moz-message-bar> + <moz-message-bar id="warningMessage" type="warning" message="Test message"></moz-message-bar> + <moz-message-bar id="successMessage" type="success" message="Test message"></moz-message-bar> + <moz-message-bar id="errorMessage" type="error" message="Test message"></moz-message-bar> +</div> +<pre id="test"> + <script class="testbody" type="application/javascript"> + add_task(async function test_component_declaration() { + const mozMessageBar = document.querySelector("#infoMessage"); + ok(mozMessageBar, "moz-message-bar component is rendered."); + + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("info-filled.svg"), "Info icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Info", "Alternate text for the info icon is present."); + + const heading = mozMessageBar.shadowRoot.querySelector(".heading"); + is(heading.textContent.trim(), "Heading", "Heading is showing up."); + const message = mozMessageBar.shadowRoot.querySelector(".message"); + is(message.textContent.trim(), "Test message", "Message is showing up."); + }); + + add_task(async function test_heading_display() { + const mozMessageBar = document.querySelector("#infoMessage2"); + let heading = mozMessageBar.shadowRoot.querySelector(".heading"); + ok(!heading, "The heading element isn't displayed if it hasn't been initialized."); + + mozMessageBar.heading = "Now there's a heading"; + await mozMessageBar.updateComplete; + heading = mozMessageBar.renderRoot.querySelector(".heading"); + is(heading.textContent.trim(), "Now there's a heading", "New heading element is displayed."); + }); + + add_task(async function test_close_button() { + const notDismissableComponent = document.querySelector("#infoMessage"); + let closeButton = notDismissableComponent.closeButtonEl; + ok(!closeButton, "Close button doesn't show when the message bar isn't dismissable."); + + let dismissableComponent = document.querySelector("#infoMessage2"); + closeButton = dismissableComponent.closeButtonEl; + ok(closeButton, "Close button is shown when the message bar is dismissable."); + + closeButton.click(); + dismissableComponent = document.querySelector("#infoMessage2"); + is(dismissableComponent, null, "Clicking on the close button removes the message bar element."); + }); + + add_task(async function test_warning_message_component() { + const mozMessageBar = document.querySelector("#warningMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("warning.svg"), "Warning icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Warning", "Alternate text for the warning icon is present."); + }); + + add_task(async function test_success_message_component() { + const mozMessageBar = document.querySelector("#successMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("check-filled.svg"), "Success icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Success", "Alternate text for the success icon is present."); + }); + + add_task(async function test_error_message_component() { + const mozMessageBar = document.querySelector("#errorMessage"); + const icon = mozMessageBar.shadowRoot.querySelector(".icon"); + const iconUrl = icon.src; + ok(iconUrl.includes("error.svg"), "Error icon is showing up."); + const iconAlt = icon.alt; + is(iconAlt, "Error", "Alternate text for the error icon is present."); + }); + </script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_support_link.html b/toolkit/content/tests/widgets/test_moz_support_link.html new file mode 100644 index 0000000000..3f523c04a6 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_support_link.html @@ -0,0 +1,151 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozSupportLink tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-support-link.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <a + id="testElement" + is="moz-support-link" + data-l10n-id="test" + support-page="support-test" + >testElement</a> + + <a + id="testElement2" + is="moz-support-link" + data-l10n-id="test2" + support-page="support-test" + utm-content="utmcontent-test" + >testElement2</a> + + <a + id="testElement3" + is="moz-support-link" + data-l10n-name="name" + support-page="support-test" + ></a> + + <a + id="testElement4" + is="moz-support-link" + data-l10n-id="test4" + data-l10n-name="name" + support-page="support-test" +></a> + + <a + id="testElement5" + is="moz-support-link" + support-page="support-test" + ></a> + +</div> +<pre id="test"></pre> +<script> + function assertBasicProperties(link, { l10nId, l10nName, supportPage, supportUrl, utmContent }) { + is(link.localName, "a", "Check that it is an anchor"); + is(link.constructor.name, "MozSupportLink", "Element should be a 'moz-support-link'"); + if (l10nId) { + is(link.getAttribute("data-l10n-id"), l10nId, "Check data-l10n-id is correct"); + } + if (l10nName) { + is(link.getAttribute("data-l10n-name"), l10nName, "Check data-l10n-name is correct"); + } + if (supportPage && utmContent) { + is(link.getAttribute("utm-content"), utmContent, "Check utm-correct is correct"); + is(link.getAttribute("support-page"), supportPage, "Check support-page is correct"); + is(link.target, "_blank", "support link should open a new window"); + let expectedHref = `${supportUrl}${supportPage}?utm_source=firefox-browser&utm_medium=firefox-browser&utm_content=${utmContent}`; + is(link.href, expectedHref, "href should be generated correctly when using utm-content"); + } else if (supportPage) { + is(link.getAttribute("support-page"), supportPage, "Check support-page is correct"); + is(link.target, "_blank", "support link should open a new window"); + is(link.href, `${supportUrl}${supportPage}`, `href should be generated SUPPORT_URL plus ${supportPage}`); + } + } + add_task(async function test_component_declaration() { + let mozSupportLink = customElements.get("moz-support-link"); + let supportUrl = mozSupportLink.SUPPORT_URL; + let supportPage = "support-test"; + + // Ensure all the semantics of the primary link are present + let supportLink = document.getElementById("testElement"); + assertBasicProperties(supportLink, {l10nId: "test", supportPage, supportUrl}); + + // Ensure AMO support link has the correct values + let supportLinkAMO = document.getElementById("testElement2"); + assertBasicProperties(supportLinkAMO, { + l10nId: "test2", + supportPage, + supportUrl, + utmContent:"utmcontent-test" + }); + + // Ensure data-l10n-name is not overwritten by the component + let supportLinkL10nName = document.getElementById("testElement3"); + assertBasicProperties(supportLinkL10nName, { + l10nId: null, + l10nName: "name", + supportPage, + supportUrl + }); + + // Ensure data-l10n-id and data-l10n-name are not overwritten by the component + let linkWithNameAndId = document.getElementById("testElement4"); + assertBasicProperties(linkWithNameAndId, { + l10nId: "test4", + l10nName: "name", + supportPage, + supportUrl + }); + + // Ensure moz-support-link without assigned data-l10n-id gets the default id + let defaultLink = document.getElementById("testElement5"); + assertBasicProperties(defaultLink, { + l10nId: "moz-support-link-text", + supportPage, + supportUrl + }); + }); + + add_task(async function test_creating_component() { + // Ensure created support link behaves as expected + let mozSupportLink = customElements.get("moz-support-link"); + let supportUrl = mozSupportLink.SUPPORT_URL; + let l10nId = "constructedElement"; + let content = document.getElementById("content"); + let utmSupportLink = document.createElement("a", { is: "moz-support-link" }); + utmSupportLink.id = l10nId; + utmSupportLink.innerText = l10nId; + document.l10n.setAttributes(utmSupportLink, l10nId); + content.appendChild(utmSupportLink); + assertBasicProperties(utmSupportLink, { supportUrl, l10nId }); + + // Set href via "support-page" after creating the element + utmSupportLink.setAttribute("support-page", "created-page"); + assertBasicProperties(utmSupportLink, { + supportUrl, + supportPage: "created-page" + }); + + // Set href via "utm-content" + utmSupportLink.setAttribute("utm-content", "created-content"); + assertBasicProperties(utmSupportLink, { + supportUrl, + supportPage: "created-page", + utmContent: "created-content" + }); + }); +</script> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_moz_toggle.html b/toolkit/content/tests/widgets/test_moz_toggle.html new file mode 100644 index 0000000000..62f248c599 --- /dev/null +++ b/toolkit/content/tests/widgets/test_moz_toggle.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>MozToggle tests</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <!-- TODO: Bug 1798404 - in-content/common.css can be removed once we have a better + solution for token variables for the new widgets --> + <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"> + <script type="module" src="chrome://global/content/elements/moz-toggle.mjs"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="max-width: fit-content"> + <moz-toggle label="Label" description="Description" pressed="true"></moz-toggle> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + add_task(async function testMozToggleDisplay() { + const mozToggle = document.querySelector("moz-toggle"); + ok(mozToggle, "moz-toggle is rendered"); + + const label = mozToggle.labelEl; + ok(label, "moz-toggle contains a label"); + ok(label.textContent.includes("Label"), "The expected label text is shown"); + + const description = mozToggle.descriptionEl; + ok(description, "moz-toggle contains a description"); + ok(description.textContent.includes("Description"), "The expected description text is shown"); + + const button = mozToggle.buttonEl; + ok(button, "moz-toggle contains a button"); + is(button.getAttribute("aria-pressed"), "true", "The button is pressed"); + }); + + add_task(async function testMozToggleInteraction() { + const mozToggle = document.querySelector("moz-toggle"); + const button = mozToggle.buttonEl; + is(mozToggle.pressed, true, "moz-toggle is pressed initially"); + is(button.getAttribute("aria-pressed"), "true", "aria-pressed reflects the pressed state"); + + synthesizeMouseAtCenter(button, {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, false, "The toggle pressed state changes on click"); + is(button.getAttribute("aria-pressed"), "false", "aria-pressed reflects this change"); + + synthesizeMouseAtCenter(mozToggle.labelEl, {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, true, "The toggle pressed state changes on label click"); + is(button.getAttribute("aria-pressed"), "true", "aria-pressed reflects this change"); + + mozToggle.focus(); + synthesizeKey(" ", {}); + await mozToggle.updateComplete; + + is(mozToggle.pressed, false, "The toggle pressed state can be changed via space bar"); + is(button.getAttribute("aria-pressed"), "false", "aria-pressed reflects this change"); + }); + + add_task(async function testSupportsAccesskey() { + const mozToggle = document.querySelector("moz-toggle"); + let nextAccesskey = "l"; + mozToggle.setAttribute("accesskey", nextAccesskey); + + synthesizeKey( + nextAccesskey, + navigator.platform.includes("Mac") + ? { altKey: true, ctrlKey: true } + : { altKey: true, shiftKey: true } + ); + + is( + mozToggle.shadowRoot.activeElement, + mozToggle.buttonEl, + "Focus has moved to the toggle button." + ); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_nac_mutations.html b/toolkit/content/tests/widgets/test_nac_mutations.html new file mode 100644 index 0000000000..3e4896bec2 --- /dev/null +++ b/toolkit/content/tests/widgets/test_nac_mutations.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<title>UA Widget mutation observer test</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<video controls id="video"></video> +<div style="overflow: scroll; width: 100px; height: 100px" id="scroller"></div> +<script> +const video = document.getElementById("video"); +const scroller = document.getElementById("scroller"); + +async function test_mutations_internal(observedNode, elementToMutate, expectMutations) { + let resolveMutations; + let mutations = new Promise(r => { + resolveMutations = r; + }); + + let observer = new MutationObserver(function(m) { + ok(expectMutations, "Mutations should be expected"); + resolveMutations(m) + }); + + SpecialPowers.wrap(observer).observe(observedNode, { + subtree: true, + attributes: true, + chromeOnlyNodes: expectMutations, + }); + + elementToMutate.setAttribute("unlikely", `value-${expectMutations}`); + + if (expectMutations) { + await mutations; + } else { + await new Promise(r => SimpleTest.executeSoon(r)); + } + + observer.disconnect(); +} + +async function test_mutations(observedNode, elementToMutate) { + for (let chromeOnlyNodes of [true, false]) { + info(`Testing chromeOnlyNodes: ${chromeOnlyNodes}`); + await test_mutations_internal(observedNode, elementToMutate, chromeOnlyNodes); + } +} + +add_task(async function test_ua_mutations() { + let shadow = SpecialPowers.wrap(video).openOrClosedShadowRoot; + ok(!!shadow, "UA Widget ShadowRoot exists"); + + await test_mutations(shadow, shadow.querySelector("*")); +}); + +add_task(async function test_scrollbar_mutations_same_anon_tree() { + let scrollbar = SpecialPowers.wrap(window).InspectorUtils.getChildrenForNode(scroller, true, false)[0]; + is(scrollbar.tagName, "scrollbar", "should find a scrollbar"); + await test_mutations(scrollbar, scrollbar); +}); + +add_task(async function test_scrollbar_mutations_same_tree() { + let scrollbar = SpecialPowers.wrap(window).InspectorUtils.getChildrenForNode(scroller, true, false)[0]; + is(scrollbar.tagName, "scrollbar", "should find a scrollbar"); + await test_mutations(scroller, scrollbar); +}); +</script> diff --git a/toolkit/content/tests/widgets/test_panel_item_accesskey.html b/toolkit/content/tests/widgets/test_panel_item_accesskey.html new file mode 100644 index 0000000000..a35e94d456 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_item_accesskey.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel Item Accesskey Support</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <panel-list> + <panel-item accesskey="F">First item</panel-item> + <panel-item accesskey="S">Second item</panel-item> + <panel-item>Third item</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + +add_task(async function testAccessKey() { + function assertAccessKeys(items, keys, { checkLabels = false } = {}) { + is(items.length, keys.length, "Got the same number of items and keys"); + for (let i = 0; i < items.length; i++) { + is(items[i].accessKey, keys[i], `Item ${i} has the right key`); + if (checkLabels) { + let label = items[i].shadowRoot.querySelector("label"); + is(label.accessKey, keys[i] || null, `Label ${i} has the right key`); + } + } + } + + let panelList = document.querySelector("panel-list"); + let panelItems = [...panelList.children]; + + info("Accesskeys should be removed when closed"); + assertAccessKeys(panelItems, ["", "", ""]); + + info("Accesskeys should be set when open"); + let panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + assertAccessKeys(panelItems, ["F", "S", ""], { checkLabels: true }); + + info("Changing accesskeys should happen right away"); + panelItems[1].accessKey = "c"; + panelItems[2].accessKey = "T"; + assertAccessKeys(panelItems, ["F", "c", "T"], { checkLabels: true }); + + info("Accesskeys are removed again on hide"); + let panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await panelHidden; + assertAccessKeys(panelItems, ["", "", ""]); + + info("Accesskeys are set again on show"); + panelItems[0].removeAttribute("accesskey"); + panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + assertAccessKeys(panelItems, ["", "c", "T"], { checkLabels: true }); + + info("Check that accesskeys can be used without the modifier when open"); + let secondClickCount = 0; + let thirdClickCount = 0; + panelItems[1].addEventListener("click", () => secondClickCount++); + panelItems[2].addEventListener("click", () => thirdClickCount++); + + // Make sure the focus is in the window. + panelItems[0].focus(); + + panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("c", {}); + await panelHidden; + + is(secondClickCount, 1, "The accesskey worked unmodified"); + is(thirdClickCount, 0, "The other listener wasn't fired"); + + synthesizeKey("c", {}); + synthesizeKey("t", {}); + + is(secondClickCount, 1, "The key doesn't trigger when closed"); + is(thirdClickCount, 0, "The key doesn't trigger when closed"); + + panelShown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await panelShown; + + panelHidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("t", {}); + await panelHidden; + + is(secondClickCount, 1, "The other listener wasn't fired"); + is(thirdClickCount, 1, "The accesskey worked unmodified"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_accessibility.html b/toolkit/content/tests/widgets/test_panel_list_accessibility.html new file mode 100644 index 0000000000..c53b48e0a9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_accessibility.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List Accessibility</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let anchorButton, panelList, panelItems; + + +add_setup(function setup() { + // Clear out the other elements so only our test content is on the page. + panelList = document.getElementById("panel-list"); + panelItems = [...panelList.children]; + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function testItemFocusOnOpen() { + ok(document.activeElement, "There is an active element"); + ok(!document.activeElement.closest("panel-list"), "Focus isn't in the list"); + + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + // Focus shouldn't enter the list on a click. + ok(!document.activeElement.closest("panel-list"), "Focus isn't in the list"); + + shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + anchorButton.focus(); + synthesizeKey(" ", {}); + await shown; + + // Focus enters list with keyboard interaction. + is(document.activeElement, panelItems[0], "The first item is focused"); + + hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + is(document.activeElement, anchorButton, "The anchor is focused again on close"); +}); + +add_task(async function testAriaAttributes() { + is(panelList.getAttribute("role"), "menu", "The panel is a menu"); + + is(panelItems.length, 3, "There are 3 items"); + for (let item of panelItems) { + is(item.button.getAttribute("role"), "menuitem", "button is a menuitem"); + } +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_anchoring.html b/toolkit/content/tests/widgets/test_panel_list_anchoring.html new file mode 100644 index 0000000000..b1e11d3e4d --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_anchoring.html @@ -0,0 +1,121 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test Panel List Anchoring</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + </panel-list> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); + let anchorButton, panelList, panelItems; + + add_setup(function setup() { + panelList = document.getElementById("panel-list"); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); + }); + + add_task(async function checkAlignment() { + async function getBounds() { + await new Promise(resolve => requestAnimationFrame(resolve)); + let button = anchorButton.getBoundingClientRect(); + let menu = panelList.getBoundingClientRect(); + return { + button: { + top: Math.round(button.top), + right: Math.round(button.right), + bottom: Math.round(button.bottom), + left: Math.round(button.left), + }, + menu: { + top: Math.round(menu.top), + right: Math.round(menu.right), + bottom: Math.round(menu.bottom), + left: Math.round(menu.left), + }, + } + }; + + async function showPanel() { + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + anchorButton.click(); + await shown; + } + + async function hidePanel() { + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await hidden; + } + + anchorButton.style.position = "fixed"; + + info("Verify menu alignment in the top left corner of the browser window"); + + anchorButton.style.top = "32px"; + anchorButton.style.right = "unset"; + anchorButton.style.bottom = "unset"; + anchorButton.style.left = "32px"; + + await showPanel(); + let bounds = await getBounds(); + is(bounds.menu.top, bounds.button.bottom, "Button's bottom matches Menu's top"); + is(bounds.menu.left, bounds.button.left, "Button and Menu have the same left value"); + await hidePanel(); + + info("Verify menu alignment in the top right corner of the browser window"); + + anchorButton.style.top = "32px"; + anchorButton.style.right = "32px"; + anchorButton.style.bottom = "unset"; + anchorButton.style.left = "unset"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.top, bounds.button.bottom, "Button's bottom matches Menu's top"); + is(bounds.menu.right, bounds.button.right, "Button and Menu have the same right value"); + await hidePanel(); + + info("Verify menu alignment in the bottom right corner of the browser window"); + + anchorButton.style.top = "unset"; + anchorButton.style.right = "32px"; + anchorButton.style.bottom = "32px"; + anchorButton.style.left = "unset"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.bottom, bounds.button.top, "Button's top matches Menu's bottom"); + is(bounds.menu.right, bounds.button.right, "Button and Menu have the same right value"); + await hidePanel(); + + info("Verify menu alignment in the bottom left corner of the browser window"); + + anchorButton.style.top = "unset"; + anchorButton.style.right = "unset"; + anchorButton.style.bottom = "32px"; + anchorButton.style.left = "32px"; + + await showPanel(); + bounds = await getBounds(); + is(bounds.menu.bottom, bounds.button.top, "Button's top matches Menu's bottom"); + is(bounds.menu.left, bounds.button.left, "Button and Menu have the same left value"); + await hidePanel(); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html b/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html new file mode 100644 index 0000000000..ea5a9fc25c --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_in_xul_panel.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List In XUL Panel</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">Open</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + <panel-item>four</panel-item> + <panel-item>five</panel-item> + <panel-item>six</panel-item> + </panel-list> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let xulPanel, anchorButton, panelList; + + +add_setup(function setup() { + // The HTML document parser doesn't let us put XUL elements in the markup, so + // we have to create the <xul:panel> programmatically with script. + let content = document.getElementById("content"); + xulPanel = document.createXULElement("panel"); + panelList = document.getElementById("panel-list"); + xulPanel.appendChild(panelList); + content.appendChild(xulPanel); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function testXULPanelOpenFromClicks() { + let xulPanelShown = BrowserTestUtils.waitForPopupEvent(xulPanel, "shown"); + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + await xulPanelShown; + + ok(panelList.hasAttribute("inxulpanel"), "Should have inxulpanel attribute set"); + + let style = window.getComputedStyle(panelList); + is(style.top, "0px", "computed top inline style should be 0px."); + is(style.left, "0px", "computed left inline style should be 0px."); + + let xulPanelHidden = BrowserTestUtils.waitForPopupEvent(xulPanel, "hidden"); + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + await xulPanelHidden; +}); + +add_task(async function testXULPanelOpenProgrammatically() { + let xulPanelShown = BrowserTestUtils.waitForPopupEvent(xulPanel, "shown"); + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + panelList.show(); + await shown; + await xulPanelShown; + + ok(panelList.hasAttribute("inxulpanel"), "Should have inxulpanel attribute set"); + let style = window.getComputedStyle(panelList); + is(style.top, "0px", "computed top inline style should be 0px."); + is(style.left, "0px", "computed left inline style should be 0px."); + + let xulPanelHidden = BrowserTestUtils.waitForPopupEvent(xulPanel, "hidden"); + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + panelList.hide(); + await hidden; + await xulPanelHidden; +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html b/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html new file mode 100644 index 0000000000..0708174a88 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_min_width_from_anchor.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test Panel List Min-width From Anchor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://global/skin/global.css" type="text/css"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <button id="anchor-button">This is a button with a long string to act as a wide anchor.</button> + <panel-list id="panel-list"> + <panel-item>one</panel-item> + <panel-item>two</panel-item> + <panel-item>three</panel-item> + <panel-item>four</panel-item> + <panel-item>five</panel-item> + <panel-item>six</panel-item> + </panel-list> +</div> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule("resource://testing-common/BrowserTestUtils.sys.mjs"); +let anchorButton, panelList; + +add_setup(function setup() { + panelList = document.getElementById("panel-list"); + anchorButton = document.getElementById("anchor-button"); + anchorButton.addEventListener("click", e => panelList.toggle(e)); +}); + +add_task(async function minWidthFromAnchor() { + let anchorWidth = anchorButton.getBoundingClientRect().width; + let shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + let panelWidth = panelList.getBoundingClientRect().width; + isnot(anchorWidth, panelWidth, "Without min-width-from-anchor, panel should not have anchor width."); + + let hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; + + panelList.toggleAttribute("min-width-from-anchor", true); + + shown = BrowserTestUtils.waitForEvent(panelList, "shown"); + synthesizeMouseAtCenter(anchorButton, {}); + await shown; + + panelWidth = panelList.getBoundingClientRect().width; + is(anchorWidth, panelWidth, "With min-width-from-anchor, panel should have anchor width."); + + hidden = BrowserTestUtils.waitForEvent(panelList, "hidden"); + synthesizeKey("Escape", {}); + await hidden; +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html b/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html new file mode 100644 index 0000000000..9f265d4cf9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_panel_list_shadow_node_anchor.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1802215 - Allow <panel-list> to be anchored to shadow DOM nodes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="./panel-list.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <link rel="stylesheet" href="./panel-list.css"/> + <link rel="stylesheet" href="./panel-item.css"/> + <script> + /** + * Define a simple custom element that includes a <button> in its + * shadow DOM to anchor a panel-list on. The TestElement is slotted, + * putting any direct descendents right after a 400px tall <div> + * with a red border. + */ + class TestElement extends HTMLElement { + static get fragment() { + const MARKUP = ` + <template> + <div style="height: 100px; border: 1px solid red;"> + <button id="anchor">Anchor</button> + </div> + <slot></slot> + </template> + `; + + let parser = new DOMParser(); + let doc = parser.parseFromString(MARKUP, "text/html"); + TestElement.template = document.importNode( + doc.querySelector("template"), + true + ); + let fragment = TestElement.template.content.cloneNode(true); + return fragment; + } + + connectedCallback() { + this.shadow = this.attachShadow({ mode: "closed" }); + this.shadow.appendChild(TestElement.fragment); + this.anchor = this.shadow.querySelector("#anchor"); + this.anchor.addEventListener("click", event => { + let panelList = this.querySelector("panel-list"); + panelList.toggle(event); + }); + } + + doClick() { + this.anchor.click(); + } + } + + customElements.define("test-element", TestElement); + + /** + * Tests that if a <panel-list> is anchored on a node within a custom + * element shadow DOM, that it anchors properly to that shadow node. + */ + add_task(async function test_panel_list_anchor_on_shadow_node() { + let testElement = document.getElementById("test-element"); + let panelList = document.getElementById("test-list"); + + let openPromise = new Promise(resolve => { + panelList.addEventListener("shown", resolve, { once: true }); + }); + testElement.doClick(); + await openPromise; + + let panelRect = panelList.getBoundingClientRect(); + let anchorRect = testElement.anchor.getBoundingClientRect(); + // Recalculate the <panel-list> rect top value relative to the top-left + // of the anchorRect. We expect the <panel-list> to be tightly anchored + // to the bottom of the <button>, so we expect this new value to be 0. + let panelTopLeftRelativeToAnchorTopLeft = panelRect.top - anchorRect.top - anchorRect.height; + is( + panelTopLeftRelativeToAnchorTopLeft, + 0, + "Panel should be tightly anchored to the bottom of the button shadow node." + ); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content"> + <test-element id="test-element"> + <panel-list id="test-list"> + <panel-item>An item</panel-item> + <panel-item>Another item</panel-item> + </panel-list> + </test-element> +</div> +<pre id="test"></pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_popupanchor.xhtml b/toolkit/content/tests/widgets/test_popupanchor.xhtml new file mode 100644 index 0000000000..5c78cf62fc --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupanchor.xhtml @@ -0,0 +1,387 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Anchor Tests" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + animate="false" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +var anchor, panel; + +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. + ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal(-ish) to " + exp); +} + +function margins(popup) { + let ret = {}; + let cs = getComputedStyle(popup); + for (let side of ["top", "right", "bottom", "left"]) { + ret[side] = parseFloat(cs.getPropertyValue("margin-" + side)); + } + return ret; +} + +function checkPositionRelativeToAnchor(side) { + var panelRect = panel.getBoundingClientRect(); + var anchorRect = anchor.getBoundingClientRect(); + switch (side) { + case "left": + case "right": + is_close(panelRect.top - margins(panel).top, anchorRect.bottom, "top of panel should be at bottom of anchor"); + break; + case "top": + case "bottom": + is_close(panelRect.right + margins(panel).left, anchorRect.left, "right of panel should be left of anchor"); + break; + default: + ok(false, "unknown side " + side); + break; + } +} + +function openSlidingPopup(position, callback) { + panel.setAttribute("flip", "slide"); + _openPopup(position, callback); +} + +function openPopup(position, callback) { + panel.setAttribute("flip", "both"); + _openPopup(position, callback); +} + +async function waitForPopupPositioned(actionFn, callback) +{ + info("waitForPopupPositioned"); + let a = new Promise(resolve => { + panel.addEventListener("popuppositioned", () => resolve(true), { once: true }); + }); + + actionFn(); + + // Ensure we get at least one event, but we might get more than one, so wait for them as needed. + // + // The double-raf ensures layout runs once. setTimeout is needed because that's how popuppositioned is scheduled. + let b = new Promise(resolve => { + requestAnimationFrame(() => requestAnimationFrame(() => { + setTimeout(() => resolve(false), 0); + })); + }); + + let gotEvent = await Promise.race([a, b]); + info("got event: " + gotEvent); + if (gotEvent) { + waitForPopupPositioned(() => {}, callback); + } else { + SimpleTest.executeSoon(callback); + } +} + +function _openPopup(position, callback) { + panel.setAttribute("side", "noside"); + panel.addEventListener("popupshown", callback, {once: true}); + panel.openPopup(anchor, position); +} + +var tests = [ + // A panel with the anchor after_end - the anchor should not move on resize + ['simpleResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + checkPositionRelativeToAnchor("right"); // should not have flipped, so still "right" + panel.sizeTo(origPanelRect.width, origPanelRect.height); + checkPositionRelativeToAnchor("right"); // should not have flipped, so still "right" + next(); + }); + }], + + ['simpleResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + var origPanelRect = panel.getBoundingClientRect(); + panel.sizeTo(100, 100); + checkPositionRelativeToAnchor("bottom"); // should not have flipped. + panel.sizeTo(origPanelRect.width, origPanelRect.height); + checkPositionRelativeToAnchor("bottom"); // should not have flipped. + next(); + }); + }], + ['flippingResizeHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50); }, + () => { + checkPositionRelativeToAnchor("left"); // check it flipped. + next(); + }); + }); + }], + ['flippingResizeVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.sizeTo(50, anchor.getBoundingClientRect().top + 50); }, + () => { + checkPositionRelativeToAnchor("top"); // check it flipped. + next(); + }); + }); + }], + + ['simpleMoveToAnchorHorizontal', 'middle', function(next) { + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", 20, 0); }, + () => { + // the anchor and the panel should have moved 20px right without flipping. + checkPositionRelativeToAnchor("right"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "after_end", -20, 0); }, + () => { + // the anchor and the panel should have moved 20px left without flipping. + checkPositionRelativeToAnchor("right"); + next(); + }); + }); + }); + }], + + ['simpleMoveToAnchorVertical', 'middle', function(next) { + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, 20); }, + () => { + // the anchor and the panel should have moved 20px down without flipping. + checkPositionRelativeToAnchor("bottom"); + waitForPopupPositioned( + () => { panel.moveToAnchor(anchor, "start_after", 0, -20) }, + () => { + // the anchor and the panel should have moved 20px up without flipping. + checkPositionRelativeToAnchor("bottom"); + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip horizontally + ['flippingMoveToAnchorHorizontal', 'middle', function(next) { + var anchorRight = anchor.getBoundingClientRect().right; + // Size the panel such that it only just fits from the left-hand side of + // the window to the right of the anchor - thus, it will fit when + // anchored to the right-hand side of the anchor. + panel.sizeTo(anchorRight - 10, 100); + openPopup("after_end", function() { + checkPositionRelativeToAnchor("right"); + // Ask for it to be anchored 1/2 way between the left edge of the window + // and the anchor right - it can't fit with the panel on the left/arrow + // on the right, so it must flip (arrow on the left, panel on the right) + var offset = Math.floor(-anchorRight / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", offset, 0), + () => { + checkPositionRelativeToAnchor("left"); + // resize back to original and move to a zero offset - it should flip back. + + panel.sizeTo(anchorRight - 10, 100); + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "after_end", 0, 0), + () => { + checkPositionRelativeToAnchor("right"); // should have flipped back. + next(); + }); + }); + }); + }], + + // Do a moveToAnchor that causes the panel to flip vertically + ['flippingMoveToAnchorVertical', 'middle', function(next) { + var anchorBottom = anchor.getBoundingClientRect().bottom; + // See comments above in flippingMoveToAnchorHorizontal, but read + // "top/bottom" instead of "left/right" + panel.sizeTo(100, anchorBottom - 10); + openPopup("start_after", function() { + checkPositionRelativeToAnchor("bottom"); + var offset = Math.floor(-anchorBottom / 2); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, offset), + () => { + checkPositionRelativeToAnchor("top"); + panel.sizeTo(100, anchorBottom - 10); + + waitForPopupPositioned( + () => panel.moveToAnchor(anchor, "start_after", 0, 0), + () => { + checkPositionRelativeToAnchor("bottom"); + next(); + }); + }); + }); + }], + + ['veryWidePanel-after_end', 'middle', function(next) { + openSlidingPopup("after_end", function() { + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested.") + next(); + }); + }); + }], + + ['veryWidePanel-before_start', 'middle', function(next) { + openSlidingPopup("before_start", function() { + waitForPopupPositioned( + () => { panel.sizeTo(window.innerWidth - 10, 60); }, + () => { + is(panel.getBoundingClientRect().width, window.innerWidth - 10, "width is what we requested") + next(); + }); + }); + }], + + ['veryTallPanel-start_after', 'middle', function(next) { + openSlidingPopup("start_after", function() { + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested.") + next(); + }); + }); + }], + + ['veryTallPanel-start_before', 'middle', function(next) { + openSlidingPopup("start_before", function() { + waitForPopupPositioned( + () => { panel.sizeTo(100, window.innerHeight - 10); }, + () => { + is(panel.getBoundingClientRect().height, window.innerHeight - 10, "height is what we requested") + next(); + }); + }); + }], + + // Tests against the anchor at the right-hand side of the window + ['afterend', 'right', function(next) { + openPopup("after_end", function() { + // when we request too far to the right/bottom, the panel gets shrunk + // and moved. The amount it is shrunk by is how far it is moved. + checkPositionRelativeToAnchor("right"); + next(); + }); + }], + + ['after_start', 'right', function(next) { + openPopup("after_start", function() { + // See above - we are still too far to the right, but the anchor is + // on the other side. + checkPositionRelativeToAnchor("right"); + next(); + }); + }], + + // Tests against the anchor at the left-hand side of the window + ['after_start', 'left', function(next) { + openPopup("after_start", function() { + var panelRect = panel.getBoundingClientRect(); + ok(panelRect.left - margins(panel).left >= 0, "panel remains within the screen"); + checkPositionRelativeToAnchor("left"); + next(); + }); + }], +] + +function runTests() { + function runNextTest() { + let result = tests.shift(); + if (!result) { + // out of tests + panel.hidePopup(); + SimpleTest.finish(); + return; + } + let [name, anchorPos, test] = result; + SimpleTest.info("sub-test " + anchorPos + "." + name + " starting"); + // first arrange for the anchor to be where the test requires it. + panel.hidePopup(); + panel.sizeTo(100, 50); + // hide all the anchors here, then later we make one of them visible. + document.getElementById("anchor-left-wrapper").style.display = "none"; + document.getElementById("anchor-middle-wrapper").style.display = "none"; + document.getElementById("anchor-right-wrapper").style.display = "none"; + switch(anchorPos) { + case 'middle': + anchor = document.getElementById("anchor-middle"); + document.getElementById("anchor-middle-wrapper").style.display = "block"; + break; + case 'left': + anchor = document.getElementById("anchor-left"); + document.getElementById("anchor-left-wrapper").style.display = "block"; + break; + case 'right': + anchor = document.getElementById("anchor-right"); + document.getElementById("anchor-right-wrapper").style.display = "block"; + break; + default: + SimpleTest.ok(false, "Bad anchorPos: " + anchorPos); + runNextTest(); + return; + } + try { + test(runNextTest); + } catch (ex) { + SimpleTest.ok(false, "sub-test " + anchorPos + "." + name + " failed: " + ex.toString() + "\n" + ex.stack); + runNextTest(); + } + } + runNextTest(); +} + +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + // anchor is set by the test runner above + panel = document.getElementById("testPanel"); + + runTests(); +}); + +]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<!-- Our tests assume at least 100px around the anchor on all sides, else the + panel may flip when we don't expect it to +--> +<div id="anchor-middle-wrapper" style="margin: 100px 100px 100px 100px;"> + <p>The anchor --> <span id="anchor-middle">v</span> <--</p> +</div> +<div id="anchor-left-wrapper" style="text-align: left; display: none;"> + <p><span id="anchor-left">v</span> <-- The anchor;</p> +</div> +<div id="anchor-right-wrapper" style="text-align: right; display: none;"> + <p>The anchor --> <span id="anchor-right">v</span></p> +</div> +</body> + +</window> diff --git a/toolkit/content/tests/widgets/test_popupreflows.xhtml b/toolkit/content/tests/widgets/test_popupreflows.xhtml new file mode 100644 index 0000000000..c3f8068779 --- /dev/null +++ b/toolkit/content/tests/widgets/test_popupreflows.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Popup Reflow Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <panel id="testPanel" + type="arrow" + noautohide="true"> + </panel> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> +<![CDATA[ +let panel, anchor; + +// A reflow observer - it just remembers the stack trace of all sync reflows +// done by the panel. +let observer = { + reflows: [], + reflow (start, end) { + // Ignore reflows triggered by native code + // (Reflows from native code only have an empty stack after the first frame) + var path = (new Error().stack).split("\n").slice(1).join(""); + if (path === "") { + return; + } + + this.reflows.push(new Error().stack); + }, + + reflowInterruptible (start, end) { + // We're not interested in interruptible reflows. Why, you ask? Because + // we've simply cargo-culted this test from browser_tabopen_reflows.js! + }, + + QueryInterface: ChromeUtils.generateQI(["nsIReflowObserver", + "nsISupportsWeakReference"]) +}; + +// A test utility that counts the reflows caused by a test function. If the +// count of reflows isn't what is expected, it causes a test failure and logs +// the stack trace of all seen reflows. +function countReflows(testfn, expected) { + return new Promise(resolve => { + observer.reflows = []; + let docShell = panel.ownerGlobal.docShell; + docShell.addWeakReflowObserver(observer); + testfn().then(() => { + docShell.removeWeakReflowObserver(observer); + SimpleTest.is(observer.reflows.length, expected, "correct number of reflows"); + if (observer.reflows.length != expected) { + SimpleTest.info("stack traces of reflows:\n" + observer.reflows.join("\n") + "\n"); + } + resolve(); + }); + }); +} + +function openPopup() { + return new Promise(resolve => { + panel.addEventListener("popupshown", function popupshown() { + resolve(); + }, {once: true}); + panel.openPopup(anchor, "before_start"); + }); +} + +// ******************** +// The actual tests... +// We only have one atm - simply open a popup. +// +function testSimplePanel() { + return openPopup(); +} + +// ******************** +// The test harness... +// +SimpleTest.waitForExplicitFinish(); + +addEventListener("load", function() { + anchor = document.getElementById("anchor"); + panel = document.getElementById("testPanel"); + + // and off we go... + countReflows(testSimplePanel, 0).then(SimpleTest.finish); +}); +]]> +</script> +<body xmlns="http://www.w3.org/1999/xhtml"> + <p>The anchor --> <span id="anchor">v</span> <--</p> +</body> +</window> diff --git a/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml new file mode 100644 index 0000000000..d1dc9c1c20 --- /dev/null +++ b/toolkit/content/tests/widgets/test_tree_column_reorder.xhtml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- + XUL Widget Test for reordering tree columns + --> +<window title="Tree" width="500" height="600" + onload="setTimeout(testtag_tree_column_reorder, 0);" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script src="tree_shared.js"/> + +<tree id="tree-column-reorder" rows="1" enableColumnDrag="true"> + <treecols> + <treecol id="col_0" label="col_0" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_1" label="col_1" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_2" label="col_2" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_3" label="col_3" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_4" label="col_4" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_5" label="col_5" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_6" label="col_6" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_7" label="col_7" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_8" label="col_8" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_9" label="col_9" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_10" label="col_10" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_11" label="col_11" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="col_12" label="col_12" flex="1"/> + </treecols> + <treechildren id="treechildren-column-reorder"> + <treeitem> + <treerow> + <treecell label="col_0"/> + <treecell label="col_1"/> + <treecell label="col_2"/> + <treecell label="col_3"/> + <treecell label="col_4"/> + <treecell label="col_5"/> + <treecell label="col_6"/> + <treecell label="col_7"/> + <treecell label="col_8"/> + <treecell label="col_9"/> + <treecell label="col_10"/> + <treecell label="col_11"/> + <treecell label="col_12"/> + </treerow> + </treeitem> + </treechildren> +</tree> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html b/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html new file mode 100644 index 0000000000..d52ec48f22 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_elementFromPoint.html @@ -0,0 +1,22 @@ +<!doctype html> +<title>UA Widget getElementFromPoint</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<div id="host" style="width: 100px; height: 100px;"></div> +<script> +const host = document.getElementById("host"); +SpecialPowers.wrap(host.attachShadow({ mode: "open"})).setIsUAWidget(); +host.shadowRoot.innerHTML = ` + <div style="width: 100px; height: 100px; background-color: green;"></div> +`; +let hostRect = host.getBoundingClientRect(); +let point = { + x: hostRect.x + 50, + y: hostRect.y + 50, +}; +is(document.elementFromPoint(point.x, point.y), host, + "Host should be found from the document"); +is(host.shadowRoot.elementFromPoint(point.x, point.y), host.shadowRoot.firstElementChild, + "Should not have retargeted UA widget content to host unnecessarily"); +</script> diff --git a/toolkit/content/tests/widgets/test_ua_widget_sandbox.html b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html new file mode 100644 index 0000000000..cc53e1c6d9 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_sandbox.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget sandbox test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const content = document.getElementById("content"); + +const div = content.appendChild(document.createElement("div")); +div.attachShadow({ mode: "open"}); +SpecialPowers.wrap(div.shadowRoot).setIsUAWidget(); + +const sandbox = SpecialPowers.Cu.getUAWidgetScope(SpecialPowers.wrap(div).nodePrincipal); + +SpecialPowers.setWrapped(sandbox, "info", SpecialPowers.wrapFor(info, sandbox)); +SpecialPowers.setWrapped(sandbox, "is", SpecialPowers.wrapFor(is, sandbox)); +SpecialPowers.setWrapped(sandbox, "ok", SpecialPowers.wrapFor(ok, sandbox)); + +const sandboxScript = function(shadowRoot) { + info("UA Widget scope tests"); + is(typeof window, "undefined", "The sandbox has no window"); + is(typeof document, "undefined", "The sandbox has no document"); + + let element = shadowRoot.host; + let doc = element.ownerDocument; + let win = doc.defaultView; + + ok(win.ShadowRoot.isInstance(shadowRoot), "shadowRoot is a ShadowRoot"); + ok(win.HTMLDivElement.isInstance(element), "Element is a <div>"); + + is("createElement" in doc, false, "No document.createElement"); + is("createElementNS" in doc, false, "No document.createElementNS"); + is("createTextNode" in doc, false, "No document.createTextNode"); + is("createComment" in doc, false, "No document.createComment"); + is("importNode" in doc, false, "No document.importNode"); + is("adoptNode" in doc, false, "No document.adoptNode"); + + is("insertBefore" in element, false, "No element.insertBefore"); + is("appendChild" in element, false, "No element.appendChild"); + is("replaceChild" in element, false, "No element.replaceChild"); + is("cloneNode" in element, false, "No element.cloneNode"); + + ok("importNodeAndAppendChildAt" in shadowRoot, "shadowRoot.importNodeAndAppendChildAt"); + ok("createElementAndAppendChildAt" in shadowRoot, "shadowRoot.createElementAndAppendChildAt"); + + info("UA Widget special methods tests"); + + const span = shadowRoot.createElementAndAppendChildAt(shadowRoot, "span"); + span.textContent = "Hello from <span>!"; + + is(shadowRoot.lastChild, span, "<span> inserted"); + + const parser = new win.DOMParser(); + let parserDoc = parser.parseFromString( + `<div xmlns="http://www.w3.org/1999/xhtml">Hello from DOMParser!</div>`, "application/xml"); + shadowRoot.importNodeAndAppendChildAt(shadowRoot, parserDoc.documentElement, true); + + ok(win.HTMLDivElement.isInstance(shadowRoot.lastChild), "<div> inserted"); + is(shadowRoot.lastChild.textContent, "Hello from DOMParser!", "Deep import node worked"); + + info("UA Widget reflectors tests"); + + win.wrappedJSObject.spanElementFromUAWidget = span; + win.wrappedJSObject.divElementFromUAWidget = shadowRoot.lastChild; +}; +SpecialPowers.Cu.evalInSandbox("this.script = " + sandboxScript.toString(), sandbox); +sandbox.script(div.shadowRoot); + +ok(SpecialPowers.wrap(HTMLSpanElement).isInstance(window.spanElementFromUAWidget), "<span> exposed"); +ok(SpecialPowers.wrap(HTMLDivElement).isInstance(window.divElementFromUAWidget), "<div> exposed"); + +try { + window.spanElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <span>"); +} + +try { + window.divElementFromUAWidget.textContent; + ok(false, "Should throw."); +} catch (err) { + ok(/denied/.test(err), "Permission denied to access <div>"); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_ua_widget_unbind.html b/toolkit/content/tests/widgets/test_ua_widget_unbind.html new file mode 100644 index 0000000000..89ae6c0f00 --- /dev/null +++ b/toolkit/content/tests/widgets/test_ua_widget_unbind.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>UA Widget unbind test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody"> + +const content = document.getElementById("content"); + +add_task(function() { + const video = document.createElement("video"); + video.controls = true; + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(video); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(video).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(video); + ok(!SpecialPowers.wrap(video).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +add_task(function() { + const marquee = document.createElement("marquee"); + ok(!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(marquee); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(marquee).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(marquee); + ok(SpecialPowers.wrap(marquee).openOrClosedShadowRoot, "UA Widget Shadow Root is not removed for marquee"); +}); + +add_task(function() { + const input = document.createElement("input"); + input.type = "date"; + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is not created"); + content.appendChild(input); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is created"); + ok(!!SpecialPowers.wrap(input).openOrClosedShadowRoot.firstChild, "Widget is constructed"); + content.removeChild(input); + ok(!SpecialPowers.wrap(input).openOrClosedShadowRoot, "UA Widget Shadow Root is removed"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols.html b/toolkit/content/tests/widgets/test_videocontrols.html new file mode 100644 index 0000000000..32cd23df6a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols.html @@ -0,0 +1,564 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/* + * Positions of the UI elements, relative to the upper-left corner of the + * <video> box. + */ +const videoWidth = 320; +const videoHeight = 240; +const videoDuration = 3.8329999446868896; + +const controlBarMargin = 9; + +const playButtonWidth = 30; +const playButtonHeight = 40; +const muteButtonWidth = 30; +const muteButtonHeight = 40; +const positionAndDurationWidth = 75; +const fullscreenButtonWidth = 30; +const fullscreenButtonHeight = 40; +const volumeSliderWidth = 48; +const volumeSliderMarginStart = 4; +const volumeSliderMarginEnd = 6; +const scrubberMargin = 9; +const scrubberWidth = videoWidth - controlBarMargin - playButtonWidth - scrubberMargin * 2 - positionAndDurationWidth - muteButtonWidth - volumeSliderMarginStart - volumeSliderWidth - volumeSliderMarginEnd - fullscreenButtonWidth - controlBarMargin; +const scrubberHeight = 40; + +// Play button is on the bottom-left +const playButtonCenterX = 0 + Math.round(playButtonWidth / 2); +const playButtonCenterY = videoHeight - Math.round(playButtonHeight / 2); +// Mute button is on the bottom-right before the full screen button and volume slider +const muteButtonCenterX = videoWidth - Math.round(muteButtonWidth / 2) - volumeSliderWidth - fullscreenButtonWidth - controlBarMargin; +const muteButtonCenterY = videoHeight - Math.round(muteButtonHeight / 2); +// Fullscreen button is on the bottom-right at the far end +const fullscreenButtonCenterX = videoWidth - Math.round(fullscreenButtonWidth / 2) - controlBarMargin; +const fullscreenButtonCenterY = videoHeight - Math.round(fullscreenButtonHeight / 2); +// Scrubber bar is between the play and mute buttons. We don't need it's +// X center, just the offset of its box. +const scrubberOffsetX = controlBarMargin + playButtonWidth + scrubberMargin; +const scrubberCenterY = videoHeight - Math.round(scrubberHeight / 2); + +const video = document.getElementById("video"); + +let requiredEvents = []; +let forbiddenEvents = []; +let receivedEvents = []; +let expectingEventPromise; + +async function isMuteButtonMuted() { + const muteButton = getElementWithinVideo(video, "muteButton"); + await new Promise(SimpleTest.executeSoon); + return muteButton.getAttribute("muted") === "true"; +} + +async function isVolumeSliderShowingCorrectVolume(expectedVolume) { + const volumeControl = getElementWithinVideo(video, "volumeControl"); + await new Promise(SimpleTest.executeSoon); + is(+volumeControl.value, expectedVolume * 100, "volume slider should match expected volume"); +} + +function forceReframe() { + // Setting display then getting the bounding rect to force a frame + // reconstruction on the video element. + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + video.getBoundingClientRect(); +} + +function captureEventThenCheck(event) { + if (event) { + info(`Received event ${event.type}.`); + receivedEvents.push(event.type); + } + + const cleanupExpectations = () => { + requiredEvents.length = 0; + forbiddenEvents.length = 0; + receivedEvents.length = 0; + } + + // If receivedEvents contains any of the forbiddenEvents, reject the expectingEventPromise. + for (const forbidden of forbiddenEvents) { + if (receivedEvents.includes(forbidden)) { + // Capture list of requiredEvents for later reporting. + const oldRequiredEvents = requiredEvents.slice(); + cleanupExpectations(); + expectingEventPromise.reject(new Error(`Got forbidden event ${forbidden} while expecting ${oldRequiredEvents}`)); + return; + } + } + + if (!requiredEvents.length) { + // We might be getting an event before we started waiting for it. That's fine, + // just early exit. + return; + } + + // We are expecting at least one event. If receivedEvents is lacking one of the + // requiredEvents, exit. + for (const required of requiredEvents) { + if (!receivedEvents.includes(required)) { + return; + } + } + + // We've received all the events we required. Resolve the expectingEventPromise. + info(`No longer waiting for expected event(s) ${requiredEvents}.`); + cleanupExpectations(); + + // Don't resolve this right away, because this is called from within event handlers and + // we want all other event handlers to have a chance to respond to this event before we + // proceed with the test. This solves problems with things like a play-pause-play, where + // some of the actions will be discarded if the video controls themselves aren't in the + // expected state. + SimpleTest.executeSoon(expectingEventPromise.resolve); +} + +function waitForEvent(required, forbidden) { + return new Promise((resolve, reject) => { + expectingEventPromise = {resolve, reject}; + + info(`Waiting for ${required}` + (forbidden ? ` but not ${forbidden}...` : `...`)); + if (Array.isArray(required)) { + requiredEvents.push(...required); + } else { + requiredEvents.push(required); + } + if (forbidden) { + if (Array.isArray(forbidden)) { + forbiddenEvents.push(...forbidden); + } else { + forbiddenEvents.push(forbidden); + } + } + + // Immediately check the received events, since the calling pattern used in this test is + // calling this method *after* the events could have been triggered. + captureEventThenCheck(); + }); +} + +async function repeatUntilSuccessful(f) { + let successful = false; + do { + try { + // Delay one event loop. + await new Promise(r => SimpleTest.executeSoon(r)); + await f(); + successful = true; + } catch (error) { + info(`repeatUntilSuccessful: error ${error}.`); + } + } while(!successful); +} + +add_task(async function setup() { + SimpleTest.requestCompleteLog(); + await SpecialPowers.pushPrefEnv({ + "set": [ + ["media.cache_size", 40000], + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ["full-screen-api.transition-duration.enter", "0 0"], + ["full-screen-api.transition-duration.leave", "0 0"], + ]}); + await new Promise(resolve => { + video.addEventListener("canplaythrough", resolve, {once: true}); + video.src = "seek_with_sound.ogg"; + }); + + video.addEventListener("play", captureEventThenCheck); + video.addEventListener("pause", captureEventThenCheck); + video.addEventListener("volumechange", captureEventThenCheck); + video.addEventListener("seeking", captureEventThenCheck); + video.addEventListener("seeked", captureEventThenCheck); + document.addEventListener("mozfullscreenchange", captureEventThenCheck); + document.addEventListener("fullscreenerror", captureEventThenCheck); + + ["mousedown", "mouseup", "dblclick", "click"] + .forEach((eventType) => { + window.addEventListener(eventType, (evt) => { + // Prevent default action of leaked events and make the tests fail. + evt.preventDefault(); + ok(false, "Event " + eventType + " in videocontrol should not leak to content;" + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + }); + }); + + // Check initial state upon load + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_playbutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function click_pausebutton() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +add_task(async function mute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); +}); + +add_task(async function unmute_volume() { + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Bug 470596: Make sure that having CSS border or padding doesn't + * break the controls (though it should move them) + */ +add_task(async function styled_video() { + video.style.border = "medium solid purple"; + video.style.borderWidth = "30px 40px 50px 60px"; + video.style.padding = "10px 20px 30px 40px"; + // totals: top: 40px, right: 60px, bottom: 80px, left: 100px + + // Click the play button + synthesizeMouse(video, 100 + playButtonCenterX, 40 + playButtonCenterY, { }); + await waitForEvent("play"); + is(video.paused, false, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Pause the video + video.pause(); + await waitForEvent("pause"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); + + // Click the mute button + synthesizeMouse(video, 100 + muteButtonCenterX, 40 + muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, true, "checking video mute state"); + + // Clear the style set + video.style.border = ""; + video.style.borderWidth = ""; + video.style.padding = ""; + + // Unmute the video + video.muted = false; + await waitForEvent("volumechange"); + is(video.paused, true, "checking video play state"); + is(video.muted, false, "checking video mute state"); +}); + +/* + * Previous tests have moved playback postion, reset it to 0. + */ +add_task(async function reset_currentTime() { + ok(true, "video position is at " + video.currentTime); + video.currentTime = 0.0; + await waitForEvent(["seeking", "seeked"]); + // Bug 477434 -- sometimes we get 0.098999 here instead of 0! + // is(video.currentTime, 0.0, "checking playback position"); + ok(true, "video position is at " + video.currentTime); +}); + +/* + * Drag the slider's thumb to the halfway point with the mouse. + */ +add_task(async function drag_slider() { + const beginDragX = scrubberOffsetX; + const endDragX = scrubberOffsetX + (scrubberWidth / 2); + const expectedTime = videoDuration / 2; + + function mousemoved(evt) { + ok(false, "Mousemove event should not be handled by content while dragging; " + + "the event was dispatched from the " + evt.target.tagName.toLowerCase() + " element."); + } + + window.addEventListener("mousemove", mousemoved); + + synthesizeMouse(video, beginDragX, scrubberCenterY, {type: "mousedown", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mousemove", button: 0}); + synthesizeMouse(video, endDragX, scrubberCenterY, {type: "mouseup", button: 0}); + await waitForEvent(["seeking", "seeked"]); + ok(true, "video position is at " + video.currentTime); + // The width of srubber is not equal in every platform as we use system default font + // in duration and position box. We can not precisely drag to expected position, so + // we just make sure the difference is within 10% of video duration. + ok(Math.abs(video.currentTime - expectedTime) < videoDuration / 10, "checking expected playback position"); + + window.removeEventListener("mousemove", mousemoved); +}); + +/* + * Click the slider at the 1/4 point with the mouse (jump backwards) + */ +add_task(async function click_slider() { + synthesizeMouse(video, scrubberOffsetX + (scrubberWidth / 4), scrubberCenterY, {}); + await waitForEvent(["seeking", "seeked"]); + ok(true, "video position is at " + video.currentTime); + // The scrubber currently just jumps towards the nearest pageIncrement point, not + // precisely to the point clicked. So, expectedTime isn't (videoDuration / 4). + // We should end up at 1.733, but sometimes we end up at 1.498. I guess + // it's timing depending if the <scale> things it's click-and-hold, or a + // single click. So, just make sure we end up less that the previous + // position. + const lastPosition = (videoDuration / 2) - 0.1; + ok(video.currentTime < lastPosition, "checking expected playback position"); + + // Set volume to 0.1 so one down arrow hit will decrease it to 0. + video.volume = 0.1; + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume should be set."); + ok(!video.muted, "Video is not muted."); +}); + +// See bug 694696. +add_task(async function change_volume() { + video.focus(); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.1, "Volume is increased."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0, "Volume should be 0."); + ok(!video.muted, "Video is not muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be 0.5."); + ok(!video.muted, "Video is not muted."); + + synthesizeKey("KEY_ArrowUp"); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(video.muted, "Video is muted."); + ok(await isMuteButtonMuted(), "Mute button says it's muted"); + + synthesizeMouse(video, muteButtonCenterX, muteButtonCenterY, {}); + await waitForEvent("volumechange"); + is(video.volume, 0.6, "Volume should be 0.6."); + ok(!video.muted, "Video is not muted."); + ok(!(await isMuteButtonMuted()), "Mute button says it's not muted"); + + await repeatUntilSuccessful(async () => { + synthesizeMouse(video, fullscreenButtonCenterX, fullscreenButtonCenterY, {}); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + + await repeatUntilSuccessful(async () => { + video.focus(); + synthesizeKey("KEY_Escape"); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + is(video.volume, 0.6, "Volume should still be 0.6"); + await isVolumeSliderShowingCorrectVolume(video.volume); + forceReframe(); + + video.focus(); + synthesizeKey("KEY_ArrowDown"); + await waitForEvent("volumechange"); + is(video.volume, 0.5, "Volume should be decreased by 0.1"); + await isVolumeSliderShowingCorrectVolume(video.volume); +}); + +add_task(async function whitespace_pause_video() { + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); + + video.focus(); + sendString(" "); + await waitForEvent("pause"); + + synthesizeMouse(video, playButtonCenterX, playButtonCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1352724: Click and hold on timeline should pause video immediately. + */ +add_task(async function click_and_hold_slider() { + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {type: "mousedown", button: 0}); + await waitForEvent(["pause", "seeking", "seeked"]); + + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent("play"); +}); + +/* + * Bug 1402877: Don't let click event dispatch through media controls to video element. + */ +add_task(async function click_event_dispatch() { + const clientScriptClickHandler = (e) => { + ok(false, "Should not receive the event"); + }; + video.addEventListener("click", clientScriptClickHandler); + + video.pause(); + await waitForEvent("pause"); + video.currentTime = 0.0; + await waitForEvent(["seeking", "seeked"]); + is(video.paused, true, "checking video play state"); + synthesizeMouse(video, scrubberOffsetX + 10, scrubberCenterY, {}); + await waitForEvent(["seeking", "seeked"]); + + video.removeEventListener("click", clientScriptClickHandler); +}); + +// Bug 1367194: Always ensure video is paused before finishing the test. +add_task(async function ensure_video_pause() { + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1452342: Make sure the cursor hides and shows on full screen mode. +add_task(async function ensure_fullscreen_cursor() { + video.removeAttribute("mozNoDynamicControls"); + video.play(); + await waitForEvent("play"); + + await repeatUntilSuccessful(async () => { + video.focus(); + await video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + const controlsSpacer = getElementWithinVideo(video, "controlsSpacer"); + is(controlsSpacer.hasAttribute("hideCursor"), true, "Cursor is hidden"); + + let delta = 1; + await SimpleTest.promiseWaitForCondition(() => { + // Wiggle the mouse a bit + synthesizeMouse(video, playButtonCenterX + delta, playButtonCenterY + delta, { type: "mousemove" }); + delta = !delta; + return !controlsSpacer.hasAttribute("hideCursor"); + }, "Waiting for hideCursor attribute to disappear"); + is(controlsSpacer.hasAttribute("hideCursor"), false, "Cursor is shown"); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + + await repeatUntilSuccessful(async () => { + await document.mozCancelFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + if (!video.paused) { + video.pause(); + await waitForEvent("pause"); + } +}); + +// Bug 1505547: Make sure the fullscreen button works if the video element is in shadow tree. +add_task(async function ensure_fullscreen_button() { + video.removeAttribute("mozNoDynamicControls"); + let host = document.getElementById("host"); + let root = host.attachShadow({ mode: "open" }); + root.appendChild(video); + forceReframe(); + + await repeatUntilSuccessful(async () => { + await video.mozRequestFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + await repeatUntilSuccessful(async () => { + // Compute the location to click on to hit the fullscreen button. + // Use the video size instead of the screen size here, because mozfullscreenchange + // does not guarantee that our document covers the screen, see bug 1575630. + const r = video.getBoundingClientRect(); + const buttonCenterX = r.right - fullscreenButtonWidth / 2 - controlBarMargin; + const buttonCenterY = r.bottom - fullscreenButtonHeight / 2; + + // Though the video no longer has mozNoDynamicControls, it sometimes appears + // in the shadow DOM without visible controls. This might happen because + // toggling the attribute doesn't force the controls to appear or disappear; + // it just affects the timed fadeout behavior. So we wiggle the mouse here + // as if we were still using dynamic controls. + synthesizeMouse(video, buttonCenterX, buttonCenterY, { type: "mousemove" }); + + info(`Clicking at ${buttonCenterX}, ${buttonCenterY}.`); + synthesizeMouse(video, buttonCenterX, buttonCenterY, {}); + await waitForEvent("mozfullscreenchange", ["fullscreenerror", "play", "pause"]); + }); + + // Restore + video.setAttribute("mozNoDynamicControls", ""); + document.getElementById("content").appendChild(video); + forceReframe(); +}); + +add_task(async function ensure_doubleclick_triggers_fullscreen() { + const { x, y } = video.getBoundingClientRect(); + info("Simulate double click on media player."); + + await repeatUntilSuccessful(async () => { + synthesizeMouse(video, x, y, { clickCount: 2 }); + // TODO: A double-click for fullscreen should *not* cause the video to play, + // but it does. Adding the "play" event to the forbidden events makes the + // test timeout. + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); + + ok(true, "Double clicking should trigger fullscreen event"); + + await repeatUntilSuccessful(async () => { + await document.mozCancelFullScreen(); + await waitForEvent("mozfullscreenchange", "fullscreenerror"); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio.html b/toolkit/content/tests/widgets/test_videocontrols_audio.html new file mode 100644 index 0000000000..ad528f4c27 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls with Audio file test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="metadata"></video> +</div> + +<pre id="test"> +<script> + + const video = document.getElementById("video"); + function loadedmetadata(event) { + SimpleTest.executeSoon(function() { + const controlBar = SpecialPowers.wrap(video).openOrClosedShadowRoot.querySelector(".controlBar"); + is(controlBar.getAttribute("fullscreen-unavailable"), "true", "Fullscreen button is hidden"); + SimpleTest.finish(); + }); + } + + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startTest); + function startTest() { + // Kick off test once audio has loaded. + video.addEventListener("loadedmetadata", loadedmetadata, { once: true }); + video.src = "audio.ogg"; + } + + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html new file mode 100644 index 0000000000..b656de0103 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_audio_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-2a.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2b.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2c.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2d.html", ref: "videocontrols_direction-2-ref.html"}, + {op: "==", test: "videocontrols_direction-2e.html", ref: "videocontrols_direction-2-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html b/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html new file mode 100644 index 0000000000..a787358394 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_clickToPlay_ariaLabel.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - clickToPlayAriaLabel</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"> + <video controls preload="auto" width="480" height="320"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const videoElems = [...document.getElementsByTagName("video")]; + const testCases = []; + + function testUI(video) { + const clickToPlay = getElementWithinVideo(video, "clickToPlay"); + ok(!!clickToPlay.getAttribute("aria-label"), "clickToPlay has aria-label attribute"); + }; + + videoElems.forEach(video => { + testCases.push(() => new Promise(resolve => { + SimpleTest.executeSoon(async () => { + const { widget } = SpecialPowers.wrap(window) + .windowGlobalChild.getActor("UAWidgets") + .widgets.get(video); + await widget.impl.Utils.l10n.translateRoots(); + testUI(video); + resolve(); + }); + })); + }); + + function executeTasks(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function start() { + executeTasks(testCases).then(SimpleTest.finish); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + window.addEventListener("load", loadevent); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html new file mode 100644 index 0000000000..39d6ff494f --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_closed_caption_menu.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - KeyHandler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"> + <track + id="track1" + kind="subtitles" + label="[test] en" + srclang="en" + src="test-webvtt-1.vtt" + /> + <track + id="track2" + kind="subtitles" + label="[test] fr" + srclang="fr" + src="test-webvtt-2.vtt" + /> + </video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + const video = document.getElementById("video"); + const closedCaptionButton = getElementWithinVideo(video, "closedCaptionButton"); + const fullscreenButton = getElementWithinVideo(video, "fullscreenButton"); + const textTrackList = getElementWithinVideo(video, "textTrackList"); + const textTrackListContainer = getElementWithinVideo(video, "textTrackListContainer"); + + function isClosedCaptionVisible() { + return !textTrackListContainer.hidden; + } + + // Setup video + tests.push(done => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}, done); + }, done => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", done); + }, cleanup); + + tests.push(done => { + info("Opening the CC menu should focus the first item in the menu"); + info("Focusing and clicking the closed caption button"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(isClosedCaptionVisible(), "The CC menu is visible"); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus"); + done(); + }); + + tests.push(done => { + info("aria-expanded should be reflected whether the CC menu is open or not"); + ok(closedCaptionButton.getAttribute("aria-expanded") === "false", "Closed CC menu has aria-expanded set to false"); + info("Focusing and clicking the closed caption button"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(isClosedCaptionVisible(), "The CC menu is visible"); + ok(closedCaptionButton.getAttribute("aria-expanded") === "true", "Open CC menu has aria-expanded set to true"); + done(); + }); + + tests.push(done => { + info("If CC menu is open, then arrow keys should navigate menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be in focus first"); + info("Pressing down arrow"); + synthesizeKey("KEY_ArrowDown"); + ok(textTrackList.children[1].matches(":focus"), "The second item in CC menu should now be in focus"); + info("Pressing up arrow"); + synthesizeKey("KEY_ArrowUp"); + ok(textTrackList.firstChild.matches(":focus"), "The first item in CC menu should be back in focus again"); + done(); + }); + + tests.push(done => { + info("Escape should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Escape key"); + synthesizeKey("KEY_Escape"); + ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + tests.push(done => { + info("Tabbing away should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Tab key 3x"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab"); + ok(fullscreenButton.matches(":focus"), "The fullscreen button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + tests.push(done => { + info("Shift + Tabbing away should close the CC menu"); + info("Opening the CC menu"); + closedCaptionButton.focus(); + synthesizeKey(" "); + info("Pressing Shift + Tab key"); + synthesizeKey("KEY_Tab", { shiftKey: true }); + ok(closedCaptionButton.matches(":focus"), "The CC button should be in focus"); + ok(!isClosedCaptionVisible(), "The CC menu should be closed"); + done(); + }); + + function cleanup(done) { + if (isClosedCaptionVisible()) { + closedCaptionButton.click(); + } + done(); + } + // add cleanup after every test + tests = tests.reduce((a, v) => a.concat([v, cleanup]), []); + + tests.push(SimpleTest.finish); + window.addEventListener("load", executeTests); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_error.html b/toolkit/content/tests/widgets/test_videocontrols_error.html new file mode 100644 index 0000000000..af90a4672a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_error.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Error</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const statusOverlay = getElementWithinVideo(video, "statusOverlay"); + const statusIcon = getElementWithinVideo(video, "statusIcon"); + const statusLabelErrorNoSource = getElementWithinVideo(video, "errorNoSource"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + }); + + add_task(async function check_normal_status() { + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + + // Wait for the fade out transition to complete in case the throbber + // shows up on slower platforms. + await SimpleTest.promiseWaitForCondition(() => statusOverlay.hidden, + "statusOverlay should not present without error"); + + ok(!statusOverlay.hasAttribute("status"), "statusOverlay should not be showing a state message."); + isnot(statusIcon.getAttribute("type"), "error", "should not show error icon"); + }); + + add_task(async function invalid_source() { + const errorType = "errorNoSource"; + + await new Promise(resolve => { + video.src = "invalid_source.ogg"; + video.addEventListener("error", () => SimpleTest.executeSoon(resolve)); + }); + + ok(!statusOverlay.hidden, `statusOverlay should show when ${errorType}`); + is(statusOverlay.getAttribute("status"), errorType, `statusOverlay should have correct error state: ${errorType}`); + is(statusIcon.getAttribute("type"), "error", `should show error icon when ${errorType}`); + isnot(statusLabelErrorNoSource.getBoundingClientRect().height, 0, + "errorNoSource status label should be visible."); + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_focus.html b/toolkit/content/tests/widgets/test_videocontrols_focus.html new file mode 100644 index 0000000000..0982947ffe --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_focus.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Focus</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.importESModule( + "resource://testing-common/BrowserTestUtils.sys.mjs" +); + +let video, controlBar, playButton; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}); + + // We must create the video after the keyboard-tab-to-all-controls pref is + // set. Otherwise, the tabindex won't be set correctly. + video = document.createElement("video"); + video.id = "video"; + video.controls = true; + video.preload = "auto"; + video.loop = true; + video.src = "video.ogg"; + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + const content = document.getElementById("content"); + content.append(video); + controlBar = getElementWithinVideo(video, "controlBar"); + playButton = getElementWithinVideo(video, "playButton"); + info("Waiting for video to load"); + // We must wait for loadeddata here, not loadedmetadata, as the first frame + // isn't shown until loadeddata occurs and the controls won't hide until the + // first frame is shown. + await BrowserTestUtils.waitForEvent(video, "loadeddata"); + + // Play and mouseout to hide the controls. + info("Playing video"); + const playing = BrowserTestUtils.waitForEvent(video, "play"); + video.play(); + await playing; + // controlBar.hidden returns true while the animation is happening. We use + // the controlbarchange event to know when it's fully hidden. Aside from + // avoiding waitForCondition, this is necessary to avoid racing with the + // animation. + const hidden = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + sendMouseEvent({type: "mouseout"}, controlBar); + info("Waiting for controls to hide"); + await hidden; +}); + +add_task(async function testShowControlsOnFocus() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing play button"); + playButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); + +add_task(async function testCcMenuStaysVisible() { + ok(controlBar.hidden, "Controls initially hidden"); + const shown = BrowserTestUtils.waitForEvent(video, "controlbarchange"); + info("Focusing CC button"); + const ccButton = getElementWithinVideo(video, "closedCaptionButton"); + ccButton.focus(); + await shown; + ok(!controlBar.hidden, "Controls shown after focus"); + // Checking this using an implementation detail is ugly, but there's no other + // way to do it without fragile timing. + const { widget } = window.windowGlobalChild.getActor("UAWidgets").widgets.get( + video); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + const ttList = getElementWithinVideo(video, "textTrackListContainer"); + ok(ttList.hidden, "Text track list initially hidden"); + + synthesizeKey(" "); + ok(!ttList.hidden, "Text track list shown after space"); + ok( + !widget.impl.Utils._hideControlsTimeout, + "Hide timeout cleared (controls won't hide)" + ); + const ccOff = ttList.querySelector("button"); + ccOff.focus(); + synthesizeKey(" "); + ok(ttList.hidden, "Text track list hidden after activating Off button"); + ok(!controlBar.hidden, "Controls still shown"); + ok(widget.impl.Utils._hideControlsTimeout, "Hide timeout set"); + + await BrowserTestUtils.waitForEvent(video, "controlbarchange"); + ok(controlBar.hidden, "Controls hidden after timeout"); +}); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html new file mode 100644 index 0000000000..0a74b25609 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_iframe_fullscreen.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +<iframe id="ifr1"></iframe> +<iframe id="ifr2" allowfullscreen></iframe> +<iframe id="ifr1" allow="fullscreen 'none'"></iframe> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const testCases = []; + + function checkIframeFullscreenAvailable(ifr) { + let video; + + return () => new Promise(resolve => { + ifr.srcdoc = `<video id="video" controls preload="auto"></video>`; + ifr.addEventListener("load", resolve); + }).then(() => new Promise(resolve => { + video = ifr.contentDocument.getElementById("video"); + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + })).then(() => new Promise(resolve => { + const available = video.ownerDocument.fullscreenEnabled; + const controlBar = getElementWithinVideo(video, "controlBar"); + + is(controlBar.getAttribute("fullscreen-unavailable") == "true", !available, "The controlbar should have an attribute marking whether fullscreen is available that corresponds to if the iframe has the allowfullscreen attribute."); + resolve(); + })); + } + + function start() { + testCases.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function load() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + for (let iframe of document.querySelectorAll("iframe")) + testCases.push(checkIframeFullscreenAvailable(iframe)); + testCases.push(SimpleTest.finish); + + window.addEventListener("load", load); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html new file mode 100644 index 0000000000..f3fdecc47f --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_jsdisabled.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function runTest(event) { + info(true, "----- test #" + testnum + " -----"); + + switch (testnum) { + case 1: + is(event.type, "timeupdate", "checking event type"); + is(video.paused, false, "checking video play state"); + video.removeEventListener("timeupdate", runTest); + + // Click to toggle play/pause (now pausing) + synthesizeMouseAtCenter(video, {}, win); + break; + + case 2: + is(event.type, "pause", "checking event type"); + is(video.paused, true, "checking video play state"); + win.close(); + + SimpleTest.finish(); + break; + + default: + ok(false, "unexpected test #" + testnum + " w/ event " + event.type); + throw new Error(`unexpected test #${testnum} w/ event ${event.type}`); + } + + testnum++; +} + +SpecialPowers.pushPrefEnv({"set": [["javascript.enabled", false]]}, startTest); + +var testnum = 1; + +var video; +function loadevent(event) { + is(win.testExpando, undefined, "expando shouldn't exist because js is disabled"); + video = win.document.querySelector("video"); + // Other events expected by the test. + video.addEventListener("timeupdate", runTest); + video.addEventListener("pause", runTest); +} + +var win; +function startTest() { + const TEST_FILE = location.href.replace("test_videocontrols_jsdisabled.html", + "file_videocontrols_jsdisabled.html"); + win = window.open(TEST_FILE); + win.addEventListener("load", loadevent); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html new file mode 100644 index 0000000000..5b771fc745 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_keyhandler.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - KeyHandler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + const video = document.getElementById("video"); + + const playButton = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubber"); + const volumeControl = getElementWithinVideo(video, "volumeControl"); + const muteButton = getElementWithinVideo(video, "muteButton"); + + // Setup video + tests.push(done => { + SpecialPowers.pushPrefEnv({"set": [ + ["media.cache_size", 40000], + ["media.videocontrols.keyboard-tab-to-all-controls", true], + ]}, done); + }, done => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", done); + }); + + // Bug 1350191, video should not seek while changing volume by + // pressing up/down arrow key after clicking the scrubber. + tests.push(done => { + video.addEventListener("play", done, { once: true }); + synthesizeMouseAtCenter(playButton, {}); + }, done => { + video.addEventListener("seeked", done, { once: true }); + synthesizeMouseAtCenter(scrubber, {}); + }, done => { + let counter = 0; + let keys = ["KEY_ArrowDown", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowDown", "KEY_ArrowUp", "KEY_ArrowUp"]; + + const onSeeked = () => ok(false, "should not trigger seeked event"); + video.addEventListener("seeked", onSeeked); + const onVolumeChange = () => { + if (++counter === keys.length) { + ok(true, "change volume by up/down arrow key without trigger 'seeked' event"); + video.removeEventListener("seeked", onSeeked); + video.removeEventListener("volumechange", onVolumeChange); + done(); + } + + if (counter > keys.length) { + ok(false, "trigger too much volumechange events"); + } + }; + video.addEventListener("volumechange", onVolumeChange); + + for (let key of keys) { + synthesizeKey(key); + } + }); + + // However, if the scrubber is *focused* (e.g. by keyboard), it should handle + // up/down arrow keys. + tests.push(done => { + info("Focusing the scrubber"); + scrubber.focus(); + video.addEventListener("seeked", () => { + ok(true, "DownArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowDown"); + }, done => { + video.addEventListener("seeked", () => { + ok(true, "UpArrow seeked the video"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowUp"); + }); + + // Similarly, if the volume control is focused, left/right arrows should + // adjust the volume. + tests.push(done => { + info("Focusing the volume control"); + volumeControl.focus(); + video.addEventListener("volumechange", () => { + ok(true, "LeftArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowLeft"); + }, done => { + video.addEventListener("volumechange", () => { + ok(true, "RightArrow changed the volume"); + done(); + }, { once: true }); + synthesizeKey("KEY_ArrowRight"); + }); + + // If something other than a button has focus, space should pause/play. + tests.push(done => { + ok(volumeControl.matches(":focus"), "Volume control still has focus"); + video.addEventListener("pause", () => { + ok(true, "Space paused the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }, done => { + video.addEventListener("play", () => { + ok(true, "Space played the video"); + done(); + }, {once: true}); + synthesizeKey(" "); + }); + + // If a button has focus, space should activate it, *not* pause/play. + tests.push(done => { + info("Focusing the mute button"); + muteButton.focus(); + const onPause = () => ok(false, "Shouldn't pause the video"); + video.addEventListener("pause", onPause); + let volChanges = 0; + const onVolChange = () => { + if (++volChanges == 2) { + ok(true, "Space twice muted then unmuted the video"); + video.removeEventListener("pause", onPause); + video.removeEventListener("volumechange", onVolChange); + done(); + } + }; + video.addEventListener("volumechange", onVolChange); + // Press space twice. The first time should mute, the second should unmute. + synthesizeKey(" "); + synthesizeKey(" "); + }); + + tests.push(SimpleTest.finish); + + window.addEventListener("load", executeTests); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html new file mode 100644 index 0000000000..9023512ab7 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_onclickplay.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls mozNoDynamicControls preload="auto"></video> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var video = document.getElementById("video"); + +function startMediaLoad() { + // Kick off test once video has loaded, in its canplaythrough event handler. + video.src = "seek_with_sound.ogg"; + video.addEventListener("canplaythrough", runTest); +} + +function loadevent(event) { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, startMediaLoad); +} + +window.addEventListener("load", loadevent); + +function runTest() { + video.addEventListener("click", function() { + this.play(); + }); + ok(video.paused, "video should be paused initially"); + + new Promise(resolve => { + let timeupdates = 0; + video.addEventListener("timeupdate", function timeupdate() { + ok(!video.paused, "video should not get paused after clicking in middle"); + + if (++timeupdates == 3) { + video.removeEventListener("timeupdate", timeupdate); + resolve(); + } + }); + + synthesizeMouseAtCenter(video, {}, window); + }).then(function() { + new Promise(resolve => { + video.addEventListener("pause", function onpause() { + setTimeout(() => { + ok(video.paused, "video should still be paused 200ms after pause request"); + // When the video reaches the end of playback it is automatically paused. + // Check during the pause event that the video has not reachd the end + // of playback. + ok(!video.ended, "video should not have paused due to playback ending"); + resolve(); + }, 200); + }); + + synthesizeMouse(video, 10, video.clientHeight - 10, {}, window); + }).then(SimpleTest.finish); + }); +} + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html new file mode 100644 index 0000000000..b1d2ab9e74 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - https://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Initial scrubber position</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" mozNoDynamicControls preload="auto"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const video = document.getElementById("video"); + +add_task(async function setup() { + await new Promise(resolve => { + video.addEventListener("canplaythrough", resolve, {once: true}); + video.src = "seek_with_sound.ogg"; + }); + + // Check initial state upon load + is(video.paused, true, "checking video play state"); +}); + +add_task(function test_initial_scrubber_position() { + // When the controls are shown after the initial video frame, + // reflowedDimensions might not be set... + video.setAttribute("controls", "true"); + + // ... but we still want to ensure the initial scrubber position + // is reasonable. + const scrubber = getElementWithinVideo(video, "scrubber"); + ok(scrubber.max, "The max value should be set on the scrubber"); + is(parseInt(scrubber.value), 0, "The initial position should be 0"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html new file mode 100644 index 0000000000..9fbb6fbcb5 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_scrubber_position_nopreload.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + - https://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Video controls test - Initial scrubber position when preload is turned off</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video width="320" height="240" id="video" mozNoDynamicControls controls="true" preload="none" src="seek_with_sound.ogg"></video> +</div> + +<div id="host"></div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const video = document.getElementById("video"); + +add_task(function test_initial_scrubber_position() { + // Check initial state upon load + is(video.paused, true, "checking video play state"); + + const scrubber = getElementWithinVideo(video, "scrubber"); + ok(scrubber.max, "The max value should be set on the scrubber"); + is(parseInt(scrubber.value), 0, "The initial position should be 0"); +}); + +add_task(async function test_scrubber_after_manual_move() { + // Kick-start the video before trying to change the scrubber. + let loadedPromise = video.readyState == video.HAVE_ENOUGH_DATA ? + Promise.resolve() : + new Promise(r => { + video.addEventListener("canplaythrough", r, {once: true}); + }); + video.play(); + await loadedPromise; + video.pause(); + const scrubber = getElementWithinVideo(video, "scrubber"); + // Click the middle of the scrubber: + synthesizeMouseAtCenter(scrubber, {}); + // Expect that the progress updates, too: + + const progress = getElementWithinVideo(video, "progressBar"); + is( + // toFixed(2) takes care of rounding issues here. + (progress.value / progress.max).toFixed(2), + (scrubber.value / scrubber.max).toFixed(2), + "Should have updated progress bar." + ); +}); + +add_task(async function test_progress_and_scrubber_once_fullscreened() { + // loop to ensure we can always get 4 timeupdate events. + video.loop = true; + video.currentTime = video.duration / 2; + info("Setting max width"); + video.style.maxWidth = "200px"; + info( + "Current video progress = " + + (video.currentTime / video.duration).toFixed(2) + ); + // Wait for a flush so the scrubber has been hidden. + await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + info("Hid progress and scrubber."); + // Then full screen. + await SpecialPowers.wrap(video).requestFullscreen(); + info("Gone into fullscreen."); + // Then wait for the video to play a bit (4 events is pretty arbitrary) + let updateCounter = 4; + let playABitPromise = new Promise(resolve => { + let handler = () => { + info("timeupdate event, counter left: " + updateCounter); + if (--updateCounter <= 0) { + video.removeEventListener("timeupdate", handler); + video.addEventListener("pause", resolve, { once: true }); + video.pause(); + } + }; + video.addEventListener("timeupdate", handler); + }); + video.play(); + await playABitPromise; + + const scrubber = getElementWithinVideo(video, "scrubber"); + let videoProgress = video.currentTime / video.duration; + let fuzzFactor = video.duration * 0.01; + info("Video progress: " + videoProgress.toFixed(3)); + let scrubberProgress = scrubber.value / scrubber.max; + info("Scrubber : " + scrubberProgress.toFixed(3)); + ok( + scrubberProgress <= videoProgress + fuzzFactor, + "Scrubber should match actual video point in time." + ); + ok( + scrubberProgress >= videoProgress - fuzzFactor, + "Scrubber should match actual video point in time." + ); + // Expect that the progress matches the scrubber: + const progress = getElementWithinVideo(video, "progressBar"); + let progressProgress = progress.value / progress.max; + info("Progress bar : " + progressProgress.toFixed(3)); + ok( + progressProgress <= videoProgress + fuzzFactor, + "Progress bar should match actual video point in time." + ); + ok( + progressProgress >= videoProgress - fuzzFactor, + "Progress bar should match actual video point in time." + ); + await SpecialPowers.wrap(document).exitFullscreen(); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_size.html b/toolkit/content/tests/widgets/test_videocontrols_size.html new file mode 100644 index 0000000000..559cc66e86 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_size.html @@ -0,0 +1,179 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - Size</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video controls preload="auto" width="480" height="320"></video> + <video controls preload="auto" width="320" height="320"></video> + <video controls preload="auto" width="280" height="320"></video> + <video controls preload="auto" width="240" height="320"></video> + <video controls preload="auto" width="180" height="320"></video> + <video controls preload="auto" width="120" height="320"></video> + <video controls preload="auto" width="60" height="320"></video> + <video controls preload="auto" width="48" height="320"></video> + <video controls preload="auto" width="20" height="320"></video> + + <video controls preload="auto" width="480" height="240"></video> + <video controls preload="auto" width="480" height="120"></video> + <video controls preload="auto" width="480" height="39"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const videoElems = [...document.getElementsByTagName("video")]; + const testCases = []; + + const isTouchControl = navigator.appVersion.includes("Android"); + + const buttonWidth = isTouchControl ? 40 : 30; + const minSrubberWidth = isTouchControl ? 64 : 48; + const minControlBarHeight = isTouchControl ? 52 : 40; + const minControlBarWidth = isTouchControl ? 58 : 48; + const minClickToPlaySize = isTouchControl ? 64 : 48; + + function getElementName(elem) { + return elem.getAttribute("anonid") || elem.getAttribute("class"); + } + + function testButton(btn) { + if (btn.hidden) return; + + const rect = btn.getBoundingClientRect(); + const name = getElementName(btn); + + is(rect.width, buttonWidth, `${name} should have correct width`); + is(rect.height, minControlBarHeight, `${name} should have correct height`); + } + + function testScrubber(scrubber) { + if (scrubber.hidden) return; + + const rect = scrubber.getBoundingClientRect(); + const name = getElementName(scrubber); + + ok(rect.width >= minSrubberWidth, `${name} should longer than ${minSrubberWidth}`); + } + + function testUI(video) { + video.style.display = "block"; + video.getBoundingClientRect(); + video.style.display = ""; + + const videoRect = video.getBoundingClientRect(); + + const videoHeight = video.clientHeight; + const videoWidth = video.clientWidth; + + const videoSizeMsg = `size:${videoRect.width}x${videoRect.height} -`; + const controlBar = getElementWithinVideo(video, "controlBar"); + const playBtn = getElementWithinVideo(video, "playButton"); + const scrubber = getElementWithinVideo(video, "scrubberStack"); + const positionDurationBox = getElementWithinVideo(video, "positionDurationBox"); + const durationLabel = positionDurationBox.getElementsByTagName("span")[0]; + const muteBtn = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + const fullscreenBtn = getElementWithinVideo(video, "fullscreenButton"); + const clickToPlay = getElementWithinVideo(video, "clickToPlay"); + + + // Controls should show/hide according to the priority + const prioritizedControls = [ + playBtn, + muteBtn, + fullscreenBtn, + positionDurationBox, + scrubber, + durationLabel, + volumeStack, + ]; + + let stopAppend = false; + prioritizedControls.forEach(control => { + is(control.hidden, stopAppend = stopAppend || control.hidden, + `${videoSizeMsg} ${getElementName(control)} should ${stopAppend ? "hide" : "show"}`); + }); + + + // All controls should fit in control bar container + const controls = [ + playBtn, + scrubber, + positionDurationBox, + muteBtn, + volumeStack, + fullscreenBtn, + ]; + + let widthSum = 0; + controls.forEach(control => { + widthSum += control.clientWidth; + }); + ok(videoWidth >= widthSum, + `${videoSizeMsg} controlBar fit in video's width`); + + + // Control bar should show/hide according to video's dimensions + const shouldHideControlBar = videoHeight <= minControlBarHeight || + videoWidth < minControlBarWidth; + is(controlBar.hidden, shouldHideControlBar, `${videoSizeMsg} controlBar show/hide`); + + if (!shouldHideControlBar) { + is(controlBar.clientWidth, videoWidth, `control bar width should equal to video width`); + + // Check all controls' dimensions + testButton(playBtn); + testButton(muteBtn); + testButton(fullscreenBtn); + testScrubber(scrubber); + testScrubber(volumeStack); + } + + + // ClickToPlay button should show if min size can fit in + const shouldHideClickToPlay = videoWidth <= minClickToPlaySize || + (videoHeight - minClickToPlaySize) / 2 <= minControlBarHeight; + is(clickToPlay.hidden, shouldHideClickToPlay, `${videoSizeMsg} clickToPlay show/hide`); + } + + + testCases.push(() => Promise.all(videoElems.map(video => new Promise(resolve => { + video.addEventListener("loadedmetadata", resolve); + video.src = "seek_with_sound.ogg"; + })))); + + videoElems.forEach(video => { + testCases.push(() => new Promise(resolve => { + SimpleTest.executeSoon(() => { + testUI(video); + resolve(); + }); + })); + }); + + function executeTasks(tasks) { + return tasks.reduce((promise, task) => promise.then(task), Promise.resolve()); + } + + function start() { + executeTasks(testCases).then(SimpleTest.finish); + } + + function loadevent() { + SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, start); + } + + window.addEventListener("load", loadevent); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_standalone.html b/toolkit/content/tests/widgets/test_videocontrols_standalone.html new file mode 100644 index 0000000000..14208923dd --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_standalone.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/NativeKeyCodes.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + SimpleTest.expectAssertions(0, 1); + +const videoWidth = 320; +const videoHeight = 240; + +function getMediaElement(aWindow) { + return aWindow.document.getElementsByTagName("video")[0]; +} + +var popup = window.open("seek_with_sound.ogg"); +popup.addEventListener("load", function() { + var video = getMediaElement(popup); + + is(popup.document.activeElement, video, "Document should load with focus moved to the video element."); + + if (!video.paused) { + runTestVideo(video); + } else { + video.addEventListener("play", function() { + runTestVideo(video); + }, {once: true}); + } +}, {once: true}); + +function runTestVideo(aVideo) { + var condition = function() { + var boundingRect = aVideo.getBoundingClientRect(); + return boundingRect.width == videoWidth && + boundingRect.height == videoHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aVideo.getBoundingClientRect(); + is(boundingRect.width, videoWidth, "Width of the video should match expectation"); + is(boundingRect.height, videoHeight, "Height of video should match expectation"); + popup.close(); + runTestAudioPre(); + }, "The media element should eventually be resized to match the intrinsic size of the video."); +} + +function runTestAudioPre() { + popup = window.open("audio.ogg"); + popup.addEventListener("load", function() { + var audio = getMediaElement(popup); + + is(popup.document.activeElement, audio, "Document should load with focus moved to the video element."); + + if (!audio.paused) { + runTestAudio(audio); + } else { + audio.addEventListener("play", function() { + runTestAudio(audio); + }, {once: true}); + } + }, {once: true}); +} + +function runTestAudio(aAudio) { + info("User agent (help diagnose bug #943556): " + navigator.userAgent); + var isAndroid = navigator.userAgent.includes("Android"); + var expectedHeight = isAndroid ? 103 : 40; + var condition = function() { + var boundingRect = aAudio.getBoundingClientRect(); + return boundingRect.height == expectedHeight; + }; + waitForCondition(condition, function() { + var boundingRect = aAudio.getBoundingClientRect(); + is(boundingRect.height, expectedHeight, + "Height of audio element should be " + expectedHeight + ", which is equal to the controls bar."); + ok(!aAudio.paused, "Should be playing"); + testPauseByKeyboard(aAudio); + }, "The media element should eventually be resized to match the height of the audio controls."); +} + +function testPauseByKeyboard(aAudio) { + aAudio.addEventListener("pause", function() { + afterKeyPause(aAudio); + }, {once: true}); + // Press spacebar, which means play/pause. + synthesizeKey(" ", {}, popup); +} + +function afterKeyPause(aAudio) { + ok(true, "successfully caused audio to pause"); + waitForCondition(function() { + return aAudio.paused; + }, + function() { + // Click outside of the controls area. (Hopefully this has no effect.) + synthesizeMouseAtPoint(5, 5, { type: 'mousedown' }, popup); + synthesizeMouseAtPoint(5, 5, { type: 'mouseup' }, popup); + setTimeout(function() { + testPlayByKeyboard(aAudio); + }, 0); + }); +} + +function testPlayByKeyboard(aAudio) { + aAudio.addEventListener("play", function() { + ok(true, "successfully caused audio to play"); + finishAudio(); + }, {once: true}); + // Press spacebar, which means play/pause. + synthesizeKey(" ", {}, popup); +} + +function finishAudio() { + popup.close(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_direction.html b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html new file mode 100644 index 0000000000..45cf7f6363 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_direction.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls directionality test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var tests = [ + {op: "==", test: "videocontrols_direction-1a.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1b.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1c.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1d.html", ref: "videocontrols_direction-1-ref.html"}, + {op: "==", test: "videocontrols_direction-1e.html", ref: "videocontrols_direction-1-ref.html"}, +]; + +</script> +<script type="text/javascript" src="videocontrols_direction_test.js"></script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html new file mode 100644 index 0000000000..bfc8018466 --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_video_noaudio.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + const video = document.getElementById("video"); + const muteButton = getElementWithinVideo(video, "muteButton"); + const volumeStack = getElementWithinVideo(video, "volumeStack"); + + add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "video.ogg"; + video.addEventListener("loadedmetadata", () => SimpleTest.executeSoon(resolve)); + }); + }); + + add_task(async function mute_button_icon() { + is(muteButton.getAttribute("noAudio"), "true"); + ok(muteButton.hasAttribute("disabled"), "Mute button should be disabled"); + + if (volumeStack) { + ok(volumeStack.hidden); + } + }); +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/test_videocontrols_vtt.html b/toolkit/content/tests/widgets/test_videocontrols_vtt.html new file mode 100644 index 0000000000..2f8d70f35a --- /dev/null +++ b/toolkit/content/tests/widgets/test_videocontrols_vtt.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Video controls test - VTT</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> + +<div id="content"> + <video id="video" controls preload="auto"></video> +</div> + +<pre id="test"> +<script clas="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + const video = document.getElementById("video"); + const ccBtn = getElementWithinVideo(video, "closedCaptionButton"); + const ttList = getElementWithinVideo(video, "textTrackList"); + const ttListContainer = getElementWithinVideo(video, "textTrackListContainer"); + + add_task(async function wait_for_media_ready() { + await SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}); + await new Promise(resolve => { + video.src = "seek_with_sound.ogg"; + video.addEventListener("loadedmetadata", resolve); + }); + }); + + add_task(async function check_inital_state() { + ok(ccBtn.hidden, "CC button should hide"); + }); + + add_task(async function check_unsupported_type_added() { + video.addTextTrack("descriptions", "English", "en"); + video.addTextTrack("chapters", "English", "en"); + video.addTextTrack("metadata", "English", "en"); + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hidden, "CC button should hide if no supported tracks provided"); + }); + + add_task(async function check_cc_button_present() { + const sub = video.addTextTrack("subtitles", "English", "en"); + sub.mode = "disabled"; + + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hidden, "CC button should show"); + is(ccBtn.hasAttribute("enabled"), false, "CC button should be disabled"); + }); + + add_task(async function check_cc_button_be_enabled() { + const subtitle = video.addTextTrack("subtitles", "English", "en"); + subtitle.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + subtitle.mode = "disabled"; + }); + + add_task(async function check_cpations_type() { + const caption = video.addTextTrack("captions", "English", "en"); + caption.mode = "showing"; + + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + }); + + add_task(async function check_track_ui_state() { + synthesizeMouseAtCenter(ccBtn, {}); + + await new Promise(SimpleTest.executeSoon); + ok(!ttListContainer.hidden, "Texttrack menu should show up"); + ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last added item should be highlighted"); + }); + + add_task(async function check_select_texttrack() { + const tt = ttList.children[1]; + + ok(tt.getAttribute("aria-checked") === "false", "Item should be off before click"); + synthesizeMouseAtCenter(tt, {}); + + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(tt.getAttribute("aria-checked") === "true", "Selected item should be enabled"); + ok(ttListContainer.hidden, "Should hide texttrack menu once clicked on an item"); + }); + + add_task(async function check_change_texttrack_mode() { + const tts = [...video.textTracks]; + + tts.forEach(tt => tt.mode = "hidden"); + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(!ccBtn.hasAttribute("enabled"), "CC button should be disabled"); + + // enable the last text track. + tts[tts.length - 1].mode = "showing"; + await once(video.textTracks, "change"); + await new Promise(SimpleTest.executeSoon); + ok(ccBtn.hasAttribute("enabled"), "CC button should be enabled"); + ok(ttList.lastChild.getAttribute("aria-checked") === "true", "The last item should be highlighted"); + }); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/content/tests/widgets/tree_shared.js b/toolkit/content/tests/widgets/tree_shared.js new file mode 100644 index 0000000000..ba52bf828e --- /dev/null +++ b/toolkit/content/tests/widgets/tree_shared.js @@ -0,0 +1,2184 @@ +// This file expects the following globals to be defined at various times. +/* globals getCustomTreeViewCellInfo */ + +// This files relies on these specific Chrome/XBL globals +/* globals TreeColumns, TreeColumn */ + +var columns_simpletree = [ + { name: "name", label: "Name", key: true, properties: "one two" }, + { name: "address", label: "Address" }, +]; + +var columns_hiertree = [ + { + name: "name", + label: "Name", + primary: true, + key: true, + properties: "one two", + }, + { name: "address", label: "Address" }, + { name: "planet", label: "Planet" }, + { name: "gender", label: "Gender", cycler: true }, +]; + +// XXXndeakin still to add some tests for: +// cycler columns, checkbox cells + +// this test function expects a tree to have 8 rows in it when it isn't +// expanded. The tree should only display four rows at a time. If editable, +// the cell at row 1 and column 0 must be editable, and the cell at row 2 and +// column 1 must not be editable. +async function testtag_tree( + treeid, + treerowinfoid, + seltype, + columnstype, + testid +) { + // Stop keystrokes that aren't handled by the tree from leaking out and + // scrolling the main Mochitests window! + function preventDefault(event) { + event.preventDefault(); + } + document.addEventListener("keypress", preventDefault); + + var multiple = seltype == "multiple"; + + var tree = document.getElementById(treeid); + var treerowinfo = document.getElementById(treerowinfoid); + var rowInfo; + if (testid == "tree view") { + rowInfo = getCustomTreeViewCellInfo(); + } else { + rowInfo = convertDOMtoTreeRowInfo(treerowinfo, 0, { value: -1 }); + } + var columnInfo = + columnstype == "simple" ? columns_simpletree : columns_hiertree; + + is(tree.selType, seltype == "multiple" ? "" : seltype, testid + " seltype"); + + // note: the functions below should be in this order due to changes made in later tests + + await testtag_tree_treecolpicker(tree, columnInfo, testid); + testtag_tree_columns(tree, columnInfo, testid); + testtag_tree_TreeSelection(tree, testid, multiple); + testtag_tree_TreeSelection_UI(tree, testid, multiple); + testtag_tree_TreeView(tree, testid, rowInfo); + + is(tree.editable, false, "tree should not be editable"); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow"); + is(tree.editingColumn, null, testid + " initial editingColumn"); + + testtag_tree_UI_editing(tree, testid, rowInfo); + + is( + tree.editable, + false, + "tree should not be editable after testtag_tree_UI_editing" + ); + // currently, the editable flag means that tree editing cannot be invoked + // by the user. However, editing can still be started with a script. + is(tree.editingRow, -1, testid + " initial editingRow (continued)"); + is(tree.editingColumn, null, testid + " initial editingColumn (continued)"); + + var ecolumn = tree.columns[0]; + ok( + !tree.startEditing(1, ecolumn), + "non-editable trees shouldn't start editing" + ); + is( + tree.editingRow, + -1, + testid + " failed startEditing shouldn't set editingRow" + ); + is( + tree.editingColumn, + null, + testid + " failed startEditing shouldn't set editingColumn" + ); + + tree.editable = true; + + ok(tree.startEditing(1, ecolumn), "startEditing should have returned true"); + is(tree.editingRow, 1, testid + " startEditing editingRow"); + is(tree.editingColumn, ecolumn, testid + " startEditing editingColumn"); + is( + tree.getAttribute("editing"), + "true", + testid + " startEditing editing attribute" + ); + + tree.stopEditing(true); + is(tree.editingRow, -1, testid + " stopEditing editingRow"); + is(tree.editingColumn, null, testid + " stopEditing editingColumn"); + is( + tree.hasAttribute("editing"), + false, + testid + " stopEditing editing attribute" + ); + + tree.startEditing(-1, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing -1 editingRow" + ); + tree.startEditing(15, ecolumn); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing 15 editingRow" + ); + tree.startEditing(1, null); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing null column editingRow" + ); + tree.startEditing(2, tree.columns[1]); + is( + tree.editingRow == -1 && tree.editingColumn == null, + true, + testid + " startEditing non editable cell editingRow" + ); + + tree.startEditing(1, ecolumn); + var inputField = tree.inputField; + is(inputField.localName, "input", testid + "inputField"); + inputField.value = "Changed Value"; + tree.stopEditing(true); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell accept" + ); + + // this cell can be edited, but stopEditing(false) means don't accept the change. + tree.startEditing(1, ecolumn); + inputField.value = "Second Value"; + tree.stopEditing(false); + is( + tree.view.getCellText(1, ecolumn), + "Changed Value", + testid + "edit cell no accept" + ); + + tree.editable = false; + + // do the sorting tests last as it will cause the rows to rearrange + // skip them for the custom tree view + if (testid != "tree view") { + testtag_tree_TreeView_rows_sort(tree, testid, rowInfo); + } + + testtag_tree_wheel(tree); + + document.removeEventListener("keypress", preventDefault); + + SimpleTest.finish(); +} + +async function testtag_tree_treecolpicker(tree, expectedColumns, testid) { + testid += " "; + + async function showAndHideTreecolpicker() { + let treecolpicker = tree.querySelector("treecolpicker"); + let treecolpickerMenupopup = treecolpicker.querySelector("menupopup"); + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popupshown", resolve, { + once: true, + }); + treecolpicker.querySelector("button").click(); + }); + let menuitems = treecolpicker.querySelectorAll("menuitem"); + // Ignore the last "Restore Column Order" menu in the count: + is( + menuitems.length - 1, + expectedColumns.length, + testid + "Same number of columns" + ); + for (var c = 0; c < expectedColumns.length; c++) { + is( + menuitems[c].textContent, + expectedColumns[c].label, + testid + "treecolpicker menu matches" + ); + ok( + !menuitems[c].querySelector("label").hidden, + testid + "label not hidden" + ); + } + await new Promise(resolve => { + treecolpickerMenupopup.addEventListener("popuphidden", resolve, { + once: true, + }); + treecolpickerMenupopup.hidePopup(); + }); + } + + // Regression test for Bug 1549931 (menuitem content being hidden upon second open) + await showAndHideTreecolpicker(); + await showAndHideTreecolpicker(); +} + +function testtag_tree_columns(tree, expectedColumns, testid) { + testid += " "; + + var columns = tree.columns; + + is( + TreeColumns.isInstance(columns), + true, + testid + "columns is a TreeColumns" + ); + is(columns.count, expectedColumns.length, testid + "TreeColumns count"); + is(columns.length, expectedColumns.length, testid + "TreeColumns length"); + + var treecols = tree.getElementsByTagName("treecols")[0]; + var treecol = treecols.getElementsByTagName("treecol"); + + var x = 0; + var primary = null, + sorted = null, + key = null; + for (var c = 0; c < expectedColumns.length; c++) { + var adjtestid = testid + " column " + c + " "; + var column = columns[c]; + var expectedColumn = expectedColumns[c]; + is(columns.getColumnAt(c), column, adjtestid + "getColumnAt"); + is( + columns.getNamedColumn(expectedColumn.name), + column, + adjtestid + "getNamedColumn" + ); + is(columns.getColumnFor(treecol[c]), column, adjtestid + "getColumnFor"); + if (expectedColumn.primary) { + primary = column; + } + if (expectedColumn.sorted) { + sorted = column; + } + if (expectedColumn.key) { + key = column; + } + + // XXXndeakin on Windows and Linux, some columns are one pixel to the + // left of where they should be. Could just be a rounding issue. + var adj = 1; + is( + column.x + adj >= x, + true, + adjtestid + + "position is after last column " + + column.x + + "," + + column.width + + "," + + x + ); + is(column.width > 0, true, adjtestid + "width is greater than 0"); + x = column.x + column.width; + + // now check the TreeColumn properties + is(TreeColumn.isInstance(column), true, adjtestid + "is a TreeColumn"); + is(column.element, treecol[c], adjtestid + "element is treecol"); + is(column.columns, columns, adjtestid + "columns is TreeColumns"); + is(column.id, expectedColumn.name, adjtestid + "name"); + is(column.index, c, adjtestid + "index"); + is(column.primary, primary == column, adjtestid + "column is primary"); + + is( + column.cycler, + "cycler" in expectedColumn && expectedColumn.cycler, + adjtestid + "column is cycler" + ); + is( + column.editable, + "editable" in expectedColumn && expectedColumn.editable, + adjtestid + "column is editable" + ); + + is( + column.type, + "type" in expectedColumn ? expectedColumn.type : 1, + adjtestid + "type" + ); + + is( + column.getPrevious(), + c > 0 ? columns[c - 1] : null, + adjtestid + "getPrevious" + ); + is( + column.getNext(), + c < columns.length - 1 ? columns[c + 1] : null, + adjtestid + "getNext" + ); + + // check the view's getColumnProperties method + var properties = tree.view.getColumnProperties(column); + var expectedProperties = expectedColumn.properties; + is( + properties, + expectedProperties ? expectedProperties : "", + adjtestid + "getColumnProperties" + ); + } + + is(columns.getFirstColumn(), columns[0], testid + "getFirstColumn"); + is( + columns.getLastColumn(), + columns[columns.length - 1], + testid + "getLastColumn" + ); + is(columns.getPrimaryColumn(), primary, testid + "getPrimaryColumn"); + is(columns.getSortedColumn(), sorted, testid + "getSortedColumn"); + is(columns.getKeyColumn(), key, testid + "getKeyColumn"); + + is(columns.getColumnAt(-1), null, testid + "getColumnAt under"); + is(columns.getColumnAt(columns.length), null, testid + "getColumnAt over"); + is(columns.getNamedColumn(""), null, testid + "getNamedColumn null"); + is( + columns.getNamedColumn("unknown"), + null, + testid + "getNamedColumn unknown" + ); + is(columns.getColumnFor(null), null, testid + "getColumnFor null"); + is(columns.getColumnFor(tree), null, testid + "getColumnFor other"); +} + +function testtag_tree_TreeSelection(tree, testid, multiple) { + testid += " selection "; + + var selection = tree.view.selection; + is( + selection instanceof Ci.nsITreeSelection, + true, + testid + "selection is a TreeSelection" + ); + is(selection.single, !multiple, testid + "single"); + + testtag_tree_TreeSelection_State(tree, testid + "initial", -1, []); + is(selection.shiftSelectPivot, -1, testid + "initial shiftSelectPivot"); + + selection.currentIndex = 2; + testtag_tree_TreeSelection_State(tree, testid + "set currentIndex", 2, []); + tree.currentIndex = 3; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex", + 3, + [] + ); + + // test the select() method, which should deselect all rows and select + // a single row + selection.select(1); + testtag_tree_TreeSelection_State(tree, testid + "select 1", 1, [1]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select 2", 3, [3]); + selection.select(3); + testtag_tree_TreeSelection_State(tree, testid + "select same", 3, [3]); + + selection.currentIndex = 1; + testtag_tree_TreeSelection_State( + tree, + testid + "set currentIndex with single selection", + 1, + [3] + ); + + tree.currentIndex = 2; + testtag_tree_TreeSelection_State( + tree, + testid + "set tree.currentIndex with single selection", + 2, + [3] + ); + + // check the toggleSelect method. In single selection mode, it only toggles on when + // there isn't currently a selection. + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect 1", + 2, + multiple ? [2, 3] : [3] + ); + selection.toggleSelect(2); + selection.toggleSelect(3); + testtag_tree_TreeSelection_State(tree, testid + "toggleSelect 2", 3, []); + + // the current index doesn't change after a selectAll, so it should still be set to 1 + // selectAll has no effect on single selection trees + selection.currentIndex = 1; + selection.selectAll(); + testtag_tree_TreeSelection_State( + tree, + testid + "selectAll 1", + 1, + multiple ? [0, 1, 2, 3, 4, 5, 6, 7] : [] + ); + selection.toggleSelect(2); + testtag_tree_TreeSelection_State( + tree, + testid + "toggleSelect after selectAll", + 2, + multiple ? [0, 1, 3, 4, 5, 6, 7] : [2] + ); + selection.clearSelection(); + testtag_tree_TreeSelection_State(tree, testid + "clearSelection", 2, []); + selection.toggleSelect(3); + selection.toggleSelect(1); + if (multiple) { + selection.selectAll(); + testtag_tree_TreeSelection_State( + tree, + testid + "selectAll 2", + 1, + [0, 1, 2, 3, 4, 5, 6, 7] + ); + } + selection.currentIndex = 2; + selection.clearSelection(); + testtag_tree_TreeSelection_State( + tree, + testid + "clearSelection after selectAll", + 2, + [] + ); + + is(selection.shiftSelectPivot, -1, testid + "shiftSelectPivot set to -1"); + + // rangedSelect and clearRange set the currentIndex to the endIndex. The + // shiftSelectPivot property will be set to startIndex. + selection.rangedSelect(1, 3, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect no augment", + multiple ? 3 : 2, + multiple ? [1, 2, 3] : [] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "shiftSelectPivot after rangedSelect no augment" + ); + if (multiple) { + selection.select(1); + selection.rangedSelect(0, 2, true); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect augment", + 2, + [0, 1, 2] + ); + is( + selection.shiftSelectPivot, + 0, + testid + "shiftSelectPivot after rangedSelect augment" + ); + + selection.clearRange(1, 3); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment", 3, [ + 0, + ]); + + // check that rangedSelect can take a start value higher than end + selection.rangedSelect(3, 1, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect reverse", + 1, + [1, 2, 3] + ); + is( + selection.shiftSelectPivot, + 3, + testid + "shiftSelectPivot after rangedSelect reverse" + ); + + // check that setting the current index doesn't change the selection + selection.currentIndex = 0; + testtag_tree_TreeSelection_State( + tree, + testid + "currentIndex with range selection", + 0, + [1, 2, 3] + ); + } + + // both values of rangedSelect may be the same + selection.rangedSelect(2, 2, false); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect one row", 2, [ + 2, + ]); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after selecting one row" + ); + + if (multiple) { + selection.rangedSelect(2, 3, true); + + // a start index of -1 means from the last point + selection.rangedSelect(-1, 0, true); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 existing selection", + 0, + [0, 1, 2, 3] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot after -1 existing selection" + ); + + selection.currentIndex = 2; + selection.rangedSelect(-1, 0, false); + testtag_tree_TreeSelection_State( + tree, + testid + "rangedSelect -1 from currentIndex", + 0, + [0, 1, 2] + ); + is( + selection.shiftSelectPivot, + 2, + testid + "shiftSelectPivot -1 from currentIndex" + ); + } + + // XXXndeakin need to test out of range values but these don't work properly + /* + selection.select(-1); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment -1", -1, []); + + selection.select(8); + testtag_tree_TreeSelection_State(tree, testid + "rangedSelect augment 8", 3, [0]); +*/ +} + +function testtag_tree_TreeSelection_UI(tree, testid, multiple) { + testid += " selection UI "; + + var selection = tree.view.selection; + selection.clearSelection(); + selection.currentIndex = 0; + tree.focus(); + + var keydownFired = 0; + var keypressFired = 0; + function keydownListener(event) { + keydownFired++; + } + function keypressListener(event) { + keypressFired++; + } + + // check that cursor up and down keys navigate up and down + // select event fires after a delay so don't expect it. The reason it fires after a delay + // is so that cursor navigation allows quicking skimming over a set of items without + // actually firing events in-between, improving performance. The select event will only + // be fired on the row where the cursor stops. + window.addEventListener("keydown", keydownListener); + window.addEventListener("keypress", keypressListener); + + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down"); + testtag_tree_TreeSelection_State(tree, testid + "key down", 1, [1], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up"); + testtag_tree_TreeSelection_State(tree, testid + "key up", 0, [0], 0); + + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up at start"); + testtag_tree_TreeSelection_State(tree, testid + "key up at start", 0, [0], 0); + + // pressing down while the last row is selected should not fire a select event, + // as the selection won't have changed. Also the view is not scrolled in this case. + selection.select(7); + synthesizeKeyExpectEvent("VK_DOWN", {}, tree, "!select", "key down at end"); + testtag_tree_TreeSelection_State(tree, testid + "key down at end", 7, [7], 0); + + // pressing keys while at the edge of the visible rows should scroll the list + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent("VK_UP", {}, tree, "!select", "key up with scroll"); + is(tree.getFirstVisibleRow(), 3, testid + "key up with scroll"); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_DOWN", + {}, + tree, + "!select", + "key down with scroll" + ); + is(tree.getFirstVisibleRow(), 1, testid + "key down with scroll"); + + // accel key and cursor movement adjust currentIndex but should not change + // the selection. In single selection mode, the selection will not change, + // but instead will just scroll up or down a line + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_DOWN", + { accelKey: true }, + tree, + "!select", + "key down with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key down with accel", + multiple ? 2 : 1, + [1] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 1, testid + "key down with accel and scroll"); + } + + tree.scrollToRow(4); + selection.select(4); + synthesizeKeyExpectEvent( + "VK_UP", + { accelKey: true }, + tree, + "!select", + "key up with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key up with accel", + multiple ? 3 : 4, + [4] + ); + if (!multiple) { + is(tree.getFirstVisibleRow(), 3, testid + "key up with accel and scroll"); + } + + // do this three times, one for each state of pageUpOrDownMovesSelection, + // and then once with the accel key pressed + for (let t = 0; t < 3; t++) { + let testidmod = ""; + if (t == 2) { + testidmod = " with accel"; + } else if (t == 1) { + testidmod = " rev"; + } + var keymod = t == 2 ? { accelKey: true } : {}; + + var moveselection = tree.pageUpOrDownMovesSelection; + if (t == 2) { + moveselection = !moveselection; + } + + tree.scrollToRow(4); + selection.currentIndex = 6; + selection.select(6); + var expected = moveselection ? 4 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up" + testidmod, + expected, + [expected], + moveselection ? 4 : 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up again" + testidmod, + expected, + [expected], + 0 + ); + + expected = moveselection ? 0 : 6; + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + keymod, + tree, + "!select", + "key page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page up at start" + testidmod, + expected, + [expected], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 1; + selection.select(1); + expected = moveselection ? 3 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down" + testidmod, + expected, + [expected], + moveselection ? 0 : 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down again" + testidmod, + expected, + [expected], + 4 + ); + + expected = moveselection ? 7 : 1; + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + keymod, + tree, + "!select", + "key page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key page down at start" + testidmod, + expected, + [expected], + 4 + ); + + if (t < 2) { + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + } + + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent("VK_HOME", {}, tree, "!select", "key home"); + testtag_tree_TreeSelection_State(tree, testid + "key home", 0, [0], 0); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent("VK_END", {}, tree, "!select", "key end"); + testtag_tree_TreeSelection_State(tree, testid + "key end", 7, [7], 4); + + // in single selection mode, the selection doesn't change in this case + tree.scrollToRow(4); + selection.select(6); + synthesizeKeyExpectEvent( + "VK_HOME", + { accelKey: true }, + tree, + "!select", + "key home with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key home with accel", + multiple ? 0 : 6, + [6], + 0 + ); + + tree.scrollToRow(0); + selection.select(1); + synthesizeKeyExpectEvent( + "VK_END", + { accelKey: true }, + tree, + "!select", + "key end with accel" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key end with accel", + multiple ? 7 : 1, + [1], + 4 + ); + + // next, test cursor navigation with selection. Here the select event will be fired + selection.select(1); + var eventExpected = multiple ? "select" : "!select"; + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift down to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to select", + multiple ? 2 : 1, + multiple ? [1, 2] : [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift down to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift up to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + multiple ? 1 : -1, + testid + "key shift up to unselect shiftSelectPivot" + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_UP", + { shiftKey: true }, + tree, + "select", + "key shift up to select" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift up to select", + 0, + [0, 1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift up to select shiftSelectPivot" + ); + synthesizeKeyExpectEvent( + "VK_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift down to unselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift down to unselect", + 1, + [1] + ); + is( + selection.shiftSelectPivot, + 1, + testid + "key shift down to unselect shiftSelectPivot" + ); + } + + // do this twice, one for each state of pageUpOrDownMovesSelection, however + // when selecting with the shift key, pageUpOrDownMovesSelection is ignored + // and the selection always changes + var lastidx = tree.view.rowCount - 1; + for (let t = 0; t < 2; t++) { + let testidmod = t == 0 ? "" : " rev"; + + // If the top or bottom visible row is the current row, pressing shift and + // page down / page up selects one page up or one page down. Otherwise, the + // selection is made to the top or bottom of the visible area. + tree.scrollToRow(lastidx - 3); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up" + testidmod, + multiple ? 4 : 6, + multiple ? [4, 5, 6] : [6] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up again" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // no change in the selection, so no select event should be fired + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up at start" + testidmod, + 0, + [0, 1, 2, 3, 4, 5, 6] + ); + // deselect by paging down again + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down deselect" + testidmod, + 3, + [3, 4, 5, 6] + ); + } + + tree.scrollToRow(1); + selection.currentIndex = 2; + selection.select(2); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down" + testidmod, + multiple ? 4 : 2, + multiple ? [2, 3, 4] : [2] + ); + if (multiple) { + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "select", + "key shift page down again" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down again" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at start" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at start" + testidmod, + 7, + [2, 3, 4, 5, 6, 7] + ); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "select", + "key shift page up deselect" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up deselect" + testidmod, + 4, + [2, 3, 4] + ); + } + + // test when page down / page up is pressed when the view is scrolled such + // that the selection is not visible + if (multiple) { + tree.scrollToRow(3); + selection.currentIndex = 1; + selection.select(1); + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + eventExpected, + "key shift page down with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down with view scrolled down" + testidmod, + 6, + [1, 2, 3, 4, 5, 6], + 3 + ); + + tree.scrollToRow(2); + selection.currentIndex = 6; + selection.select(6); + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + eventExpected, + "key shift page up with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page up with view scrolled up" + testidmod, + 2, + [2, 3, 4, 5, 6], + 2 + ); + + tree.scrollToRow(2); + selection.currentIndex = 0; + selection.select(0); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_UP", + { shiftKey: true }, + tree, + "!select", + "key shift page up at start with view scrolled down" + ); + testtag_tree_TreeSelection_State( + tree, + testid + + "key shift page up at start with view scrolled down" + + testidmod, + 0, + [0], + 0 + ); + + tree.scrollToRow(0); + selection.currentIndex = 7; + selection.select(7); + // don't expect the select event, as the selection won't have changed + synthesizeKeyExpectEvent( + "VK_PAGE_DOWN", + { shiftKey: true }, + tree, + "!select", + "key shift page down at end with view scrolled up" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift page down at end with view scrolled up" + testidmod, + 7, + [7], + 4 + ); + } + + tree.pageUpOrDownMovesSelection = !tree.pageUpOrDownMovesSelection; + } + + tree.scrollToRow(4); + selection.select(5); + synthesizeKeyExpectEvent( + "VK_HOME", + { shiftKey: true }, + tree, + eventExpected, + "key shift home" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift home", + multiple ? 0 : 5, + multiple ? [0, 1, 2, 3, 4, 5] : [5], + multiple ? 0 : 4 + ); + + tree.scrollToRow(0); + selection.select(3); + synthesizeKeyExpectEvent( + "VK_END", + { shiftKey: true }, + tree, + eventExpected, + "key shift end" + ); + testtag_tree_TreeSelection_State( + tree, + testid + "key shift end", + multiple ? 7 : 3, + multiple ? [3, 4, 5, 6, 7] : [3], + multiple ? 4 : 0 + ); + + // pressing space selects a row, pressing accel + space unselects a row + selection.select(2); + selection.currentIndex = 4; + synthesizeKeyExpectEvent(" ", {}, tree, "select", "key space on"); + // in single selection mode, space shouldn't do anything + testtag_tree_TreeSelection_State( + tree, + testid + "key space on", + 4, + multiple ? [2, 4] : [2] + ); + + if (multiple) { + synthesizeKeyExpectEvent( + " ", + { accelKey: true }, + tree, + "select", + "key space off" + ); + testtag_tree_TreeSelection_State(tree, testid + "key space off", 4, [2]); + } + + // check that clicking on a row selects it + tree.scrollToRow(0); + selection.select(2); + selection.currentIndex = 2; + if (0) { + // XXXndeakin disable these tests for now + mouseOnCell(tree, 1, tree.columns[1], "mouse on row"); + testtag_tree_TreeSelection_State( + tree, + testid + "mouse on row", + 1, + [1], + 0, + null + ); + } + + // restore the scroll position to the start of the page + sendKey("HOME"); + + window.removeEventListener("keydown", keydownListener); + window.removeEventListener("keypress", keypressListener); + is(keydownFired, multiple ? 63 : 40, "keydown event wasn't fired properly"); + is(keypressFired, multiple ? 2 : 1, "keypress event wasn't fired properly"); +} + +function testtag_tree_UI_editing(tree, testid, rowInfo) { + testid += " editing UI "; + + // check editing UI + var ecolumn = tree.columns[0]; + var rowIndex = 2; + + // temporary make the tree editable to test mouse double click + var wasEditable = tree.editable; + if (!wasEditable) { + tree.editable = true; + } + + // if this is a container save its current open status + var row = rowInfo.rows[rowIndex]; + var wasOpen = null; + if (tree.view.isContainer(row)) { + wasOpen = tree.view.isContainerOpen(row); + } + + mouseDblClickOnCell(tree, rowIndex, ecolumn, testid + "edit on double click"); + is(tree.editingColumn, ecolumn, testid + "editing column"); + is(tree.editingRow, rowIndex, testid + "editing row"); + + // ensure that we don't expand an expandable container on edit + if (wasOpen != null) { + is( + tree.view.isContainerOpen(row), + wasOpen, + testid + "opened container node on edit" + ); + } + + // ensure to restore editable attribute + if (!wasEditable) { + tree.editable = false; + } + + var ci = tree.currentIndex; + + // cursor navigation should not change the selection while editing + var testKey = function (key) { + synthesizeKeyExpectEvent( + key, + {}, + tree, + "!select", + "key " + key + " with editing" + ); + is( + tree.editingRow == rowIndex && + tree.editingColumn == ecolumn && + tree.currentIndex == ci, + true, + testid + "key " + key + " while editing" + ); + }; + + testKey("VK_DOWN"); + testKey("VK_UP"); + testKey("VK_PAGE_DOWN"); + testKey("VK_PAGE_UP"); + testKey("VK_HOME"); + testKey("VK_END"); + + // XXXndeakin figure out how to send characters to the textbox + // inputField.inputField.focus() + // synthesizeKeyExpectEvent(inputField.inputField, "b", null, ""); + // tree.stopEditing(true); + // is(tree.view.getCellText(0, ecolumn), "b", testid + "edit cell"); + + // Restore initial state. + tree.stopEditing(false); +} + +function testtag_tree_TreeView(tree, testid, rowInfo) { + testid += " view "; + + var columns = tree.columns; + var view = tree.view; + + is(view instanceof Ci.nsITreeView, true, testid + "view is a TreeView"); + is(view.rowCount, rowInfo.rows.length, testid + "rowCount"); + + testtag_tree_TreeView_rows(tree, testid, rowInfo, 0); + + // note that this will only work for content trees currently + view.setCellText(0, columns[1], "Changed Value"); + is(view.getCellText(0, columns[1]), "Changed Value", "setCellText"); + + view.setCellValue(1, columns[0], "Another Changed Value"); + is(view.getCellValue(1, columns[0]), "Another Changed Value", "setCellText"); +} + +function testtag_tree_TreeView_rows(tree, testid, rowInfo, startRow) { + var r; + var columns = tree.columns; + var view = tree.view; + var length = rowInfo.rows.length; + + // methods to test along with the functions which determine the expected value + var checkRowMethods = { + isContainer(row) { + return row.container; + }, + isContainerOpen(row) { + return false; + }, + isContainerEmpty(row) { + return row.children != null && !row.children.rows.length; + }, + isSeparator(row) { + return row.separator; + }, + getRowProperties(row) { + return row.properties; + }, + getLevel(row) { + return row.level; + }, + getParentIndex(row) { + return row.parent; + }, + hasNextSibling(row) { + return r < startRow + length - 1; + }, + }; + + var checkCellMethods = { + getCellText(row, cell) { + return cell.label; + }, + getCellValue(row, cell) { + return cell.value; + }, + getCellProperties(row, cell) { + return cell.properties; + }, + isEditable(row, cell) { + return cell.editable; + }, + getImageSrc(row, cell) { + return cell.image; + }, + }; + + var failedMethods = {}; + var checkMethod, actual, expected; + var toggleOpenStateOK = true; + + for (r = startRow; r < length; r++) { + var row = rowInfo.rows[r]; + for (var c = 0; c < row.cells.length; c++) { + var cell = row.cells[c]; + + for (checkMethod in checkCellMethods) { + expected = checkCellMethods[checkMethod](row, cell); + actual = view[checkMethod](r, columns[c]); + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + + "row " + + r + + " column " + + c + + " " + + checkMethod + + " is incorrect" + ); + } + } + } + + // compare row properties + for (checkMethod in checkRowMethods) { + expected = checkRowMethods[checkMethod](row, r); + if (checkMethod == "hasNextSibling") { + actual = view[checkMethod](r, r); + } else { + actual = view[checkMethod](r); + } + if (actual !== expected) { + failedMethods[checkMethod] = true; + is( + actual, + expected, + testid + "row " + r + " " + checkMethod + " is incorrect" + ); + } + } + /* + // open and recurse into containers + if (row.container) { + view.toggleOpenState(r); + if (!view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), true, testid + "row " + r + " toggleOpenState open"); + } + testtag_tree_TreeView_rows(tree, testid + "container " + r + " ", row.children, r + 1); + view.toggleOpenState(r); + if (view.isContainerOpen(r)) { + toggleOpenStateOK = false; + is(view.isContainerOpen(r), false, testid + "row " + r + " toggleOpenState close"); + } + } +*/ + } + + for (var failedMethod in failedMethods) { + if (failedMethod in checkRowMethods) { + delete checkRowMethods[failedMethod]; + } + if (failedMethod in checkCellMethods) { + delete checkCellMethods[failedMethod]; + } + } + + for (checkMethod in checkRowMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + for (checkMethod in checkCellMethods) { + is(checkMethod + " ok", checkMethod + " ok", testid + checkMethod); + } + if (toggleOpenStateOK) { + is("toggleOpenState ok", "toggleOpenState ok", testid + "toggleOpenState"); + } +} + +function testtag_tree_TreeView_rows_sort(tree, testid, rowInfo) { + // check if cycleHeader sorts the columns + var columnIndex = 0; + var view = tree.view; + var column = tree.columns[columnIndex]; + var columnElement = column.element; + var sortkey = columnElement.getAttribute("sort"); + if (sortkey) { + view.cycleHeader(column); + is(tree.getAttribute("sort"), sortkey, "cycleHeader sort"); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending" + ); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "cycleHeader column sortDirection" + ); + is( + columnElement.getAttribute("sortActive"), + "true", + "cycleHeader column sortActive" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection descending" + ); + is( + columnElement.getAttribute("sortDirection"), + "descending", + "cycleHeader column sortDirection descending" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "", + "cycleHeader sortDirection natural" + ); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection natural" + ); + // XXXndeakin content view isSorted needs to be tested + } + + // Check that clicking on column header sorts the column. + var columns = getSortedColumnArray(tree); + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + // Click once on column header and check sorting has cycled once. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + is( + columnElement.getAttribute("sortDirection"), + "ascending", + "single click cycleHeader column sortDirection ascending" + ); + + // Now simulate a double click. + mouseClickOnColumnHeader(columns, columnIndex, 0, 2); + if (navigator.platform.indexOf("Win") == 0) { + // Windows cycles only once on double click. + is( + columnElement.getAttribute("sortDirection"), + "descending", + "double click cycleHeader column sortDirection descending" + ); + // 1 single clicks should restore natural sorting. + mouseClickOnColumnHeader(columns, columnIndex, 0, 1); + } + + // Check we have gone back to natural sorting. + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection" + ); + + columnElement.setAttribute("sorthints", "twostate"); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "descending", + "cycleHeader sortDirection ascending twostate" + ); + view.cycleHeader(column); + is( + tree.getAttribute("sortDirection"), + "ascending", + "cycleHeader sortDirection ascending twostate again" + ); + columnElement.removeAttribute("sorthints"); + view.cycleHeader(column); + view.cycleHeader(column); + + is( + columnElement.getAttribute("sortDirection"), + "", + "cycleHeader column sortDirection reset" + ); +} + +// checks if the current and selected rows are correct +// current is the index of the current row +// selected is an array of the indicies of the selected rows +// viewidx is the row that should be visible at the top of the tree +function testtag_tree_TreeSelection_State( + tree, + testid, + current, + selected, + viewidx +) { + var selection = tree.view.selection; + + is(selection.count, selected.length, testid + " count"); + is(tree.currentIndex, current, testid + " currentIndex"); + is(selection.currentIndex, current, testid + " TreeSelection currentIndex"); + if (viewidx !== null && viewidx !== undefined) { + is(tree.getFirstVisibleRow(), viewidx, testid + " first visible row"); + } + + var actualSelected = []; + var count = tree.view.rowCount; + for (var s = 0; s < count; s++) { + if (selection.isSelected(s)) { + actualSelected.push(s); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " selection [" + selected + "]" + ); + + actualSelected = []; + var rangecount = selection.getRangeCount(); + for (var r = 0; r < rangecount; r++) { + var start = {}, + end = {}; + selection.getRangeAt(r, start, end); + for (var rs = start.value; rs <= end.value; rs++) { + actualSelected.push(rs); + } + } + + is( + compareArrays(selected, actualSelected), + true, + testid + " range selection [" + selected + "]" + ); +} + +function testtag_tree_column_reorder() { + // Make sure the tree is scrolled into the view, otherwise the test will + // fail + var testframe = window.parent.document.getElementById("testframe"); + if (testframe) { + testframe.scrollIntoView(); + } + + var tree = document.getElementById("tree-column-reorder"); + var numColumns = tree.columns.count; + + var reference = []; + for (let i = 0; i < numColumns; i++) { + reference.push("col_" + i); + } + + // Drag the first column to each position + for (let i = 0; i < numColumns - 1; i++) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag first column right"); + } + + // And back + for (let i = numColumns - 1; i >= 1; i--) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag last column left"); + } + + // Drag each column one column left + for (let i = 1; i < numColumns; i++) { + synthesizeColumnDrag(tree, i, i - 1, false); + arrayMove(reference, i, i - 1, false); + checkColumns(tree, reference, "drag each column left"); + } + + // And back + for (let i = numColumns - 2; i >= 0; i--) { + synthesizeColumnDrag(tree, i, i + 1, true); + arrayMove(reference, i, i + 1, true); + checkColumns(tree, reference, "drag each column right"); + } + + // Drag each column 5 to the right + for (let i = 0; i < numColumns - 5; i++) { + synthesizeColumnDrag(tree, i, i + 5, true); + arrayMove(reference, i, i + 5, true); + checkColumns(tree, reference, "drag each column 5 to the right"); + } + + // And to the left + for (let i = numColumns - 6; i >= 5; i--) { + synthesizeColumnDrag(tree, i, i - 5, false); + arrayMove(reference, i, i - 5, false); + checkColumns(tree, reference, "drag each column 5 to the left"); + } + + // Test that moving a column after itself does not move anything + synthesizeColumnDrag(tree, 0, 0, true); + checkColumns(tree, reference, "drag to itself"); + is(document.treecolDragging, null, "drag to itself completed"); + + // XXX roc should this be here??? + SimpleTest.finish(); +} + +function testtag_tree_wheel(aTree) { + const deltaModes = [ + WheelEvent.DOM_DELTA_PIXEL, // 0 + WheelEvent.DOM_DELTA_LINE, // 1 + WheelEvent.DOM_DELTA_PAGE, // 2 + ]; + function helper(aStart, aDelta, aIntDelta, aDeltaMode) { + aTree.scrollToRow(aStart); + var expected; + if (!aIntDelta) { + expected = aStart; + } else if (aDeltaMode != WheelEvent.DOM_DELTA_PAGE) { + expected = aStart + aIntDelta; + } else if (aIntDelta > 0) { + expected = aStart + aTree.getPageLength(); + } else { + expected = aStart - aTree.getPageLength(); + } + + if (expected < 0) { + expected = 0; + } + if (expected > aTree.view.rowCount - aTree.getPageLength()) { + expected = aTree.view.rowCount - aTree.getPageLength(); + } + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaY: aDelta, + lineOrPageDeltaY: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + expected, + "testtag_tree_wheel: vertical, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + + aTree.scrollToRow(aStart); + // Check that horizontal scrolling has no effect + synthesizeWheel(aTree.body, 1, 1, { + deltaMode: aDeltaMode, + deltaX: aDelta, + lineOrPageDeltaX: aIntDelta, + }); + is( + aTree.getFirstVisibleRow(), + aStart, + "testtag_tree_wheel: horizontal, starting " + + aStart + + " delta " + + aDelta + + " lineOrPageDelta " + + aIntDelta + + " aDeltaMode " + + aDeltaMode + ); + } + + var defaultPrevented = 0; + + function wheelListener(event) { + defaultPrevented++; + } + window.addEventListener("wheel", wheelListener); + + deltaModes.forEach(function (aDeltaMode) { + var delta = aDeltaMode == WheelEvent.DOM_DELTA_PIXEL ? 5.0 : 0.3; + helper(2, -delta, 0, aDeltaMode); + helper(2, -delta, -1, aDeltaMode); + helper(2, delta, 0, aDeltaMode); + helper(2, delta, 1, aDeltaMode); + helper(2, -2 * delta, 0, aDeltaMode); + helper(2, -2 * delta, -1, aDeltaMode); + helper(2, 2 * delta, 0, aDeltaMode); + helper(2, 2 * delta, 1, aDeltaMode); + }); + + window.removeEventListener("wheel", wheelListener); + is(defaultPrevented, 48, "wheel event default prevented"); +} + +async function testtag_tree_scroll() { + const tree = document.querySelector("tree"); + + info("Scroll down with the content scrollbar at the top"); + await doScrollTest({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: true, + }); + + info("Scroll down with the content scrollbar at the middle"); + await doScrollTest({ + tree, + initialTreeScrollRow: 3, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: true, + }); + + info("Scroll down with the content scrollbar at the bottom"); + await doScrollTest({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 0, + scrollDelta: 10, + isTreeScrollExpected: false, + }); + + info("Scroll up with the content scrollbar at the bottom"); + await doScrollTest({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: true, + }); + + info("Scroll up with the content scrollbar at the middle"); + await doScrollTest({ + tree, + initialTreeScrollRow: 5, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: true, + }); + + info("Scroll up with the content scrollbar at the top"); + await doScrollTest({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 50, + scrollDelta: -10, + isTreeScrollExpected: false, + }); + + info("Check whether the tree is not scrolled when the parent is scrolling"); + await doScrollWhileScrollingParent(tree); + + info( + "Check whether the tree component consumes wheel events even if the scroll is located at edge as long as the events are handled as the same series" + ); + await doScrollInSameSeries({ + tree, + initialTreeScrollRow: 0, + initialContainerScrollTop: 0, + scrollDelta: 10, + }); + await doScrollInSameSeries({ + tree, + initialTreeScrollRow: 9, + initialContainerScrollTop: 50, + scrollDelta: -10, + }); + + SimpleTest.finish(); +} + +async function doScrollInSameSeries({ + tree, + initialTreeScrollRow, + initialContainerScrollTop, + scrollDelta, +}) { + // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel + // event fired as the same series. + Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); + + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const parent = tree.parentElement; + + tree.scrollToRow(initialTreeScrollRow); + parent.scrollTop = initialContainerScrollTop; + + // Scroll until the scrollbar was moved to the specified amount. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(tree, 10, 10, scrollDelta); + const curpos = scrollbar.getAttribute("curpos"); + return ( + (scrollDelta < 0 && curpos == 0) || + (scrollDelta > 0 && curpos == scrollbar.getAttribute("maxpos")) + ); + }); + + // More scroll as the same series. + for (let i = 0; i < 10; i++) { + await nativeScroll(tree, 10, 10, scrollDelta); + } + + is( + parent.scrollTop, + initialContainerScrollTop, + "The wheel events are condumed in tree component" + ); + const utils = SpecialPowers.getDOMWindowUtils(window); + ok(!utils.getWheelScrollTarget(), "The parent should not handle the event"); + + Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); +} + +async function doScrollWhileScrollingParent(tree) { + // Set enough value to mousewheel.scroll_series_timeout pref to ensure the wheel + // event fired as the same series. + Services.prefs.setIntPref("mousewheel.scroll_series_timeout", 1000); + + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const parent = tree.parentElement; + + // Set initial scroll amount. + tree.scrollToRow(0); + parent.scrollTop = 0; + + const scrollAmount = scrollbar.getAttribute("curpos"); + + // Scroll parent from top to bottom. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(parent, 10, 10, 10); + return parent.scrollTop === parent.scrollTopMax; + }); + + is( + scrollAmount, + scrollbar.getAttribute("curpos"), + "The tree should not be scrolled" + ); + + const utils = SpecialPowers.getDOMWindowUtils(window); + await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); + Services.prefs.clearUserPref("mousewheel.scroll_series_timeout"); +} + +async function doScrollTest({ + tree, + initialTreeScrollRow, + initialContainerScrollTop, + scrollDelta, + isTreeScrollExpected, +}) { + const scrollbar = tree.shadowRoot.querySelector( + "scrollbar[orient='vertical']" + ); + const container = tree.parentElement; + + // Set initial scroll amount. + tree.scrollToRow(initialTreeScrollRow); + container.scrollTop = initialContainerScrollTop; + + const treeScrollAmount = scrollbar.getAttribute("curpos"); + const containerScrollAmount = container.scrollTop; + + // Wait until changing either scroll. + await SimpleTest.promiseWaitForCondition(async () => { + await nativeScroll(tree, 10, 10, scrollDelta); + return ( + treeScrollAmount !== scrollbar.getAttribute("curpos") || + containerScrollAmount !== container.scrollTop + ); + }); + + is( + treeScrollAmount !== scrollbar.getAttribute("curpos"), + isTreeScrollExpected, + "Scroll of tree is expected" + ); + is( + containerScrollAmount !== container.scrollTop, + !isTreeScrollExpected, + "Scroll of container is expected" + ); + + // Wait until finishing wheel scroll transaction. + const utils = SpecialPowers.getDOMWindowUtils(window); + await SimpleTest.promiseWaitForCondition(() => !utils.getWheelScrollTarget()); +} + +async function nativeScroll(component, offsetX, offsetY, scrollDelta) { + const utils = SpecialPowers.getDOMWindowUtils(window); + const x = component.screenX + offsetX; + const y = component.screenY + offsetY; + + // Mouse move event. + await new Promise(resolve => { + info("waiting for mousemove"); + window.addEventListener("mousemove", resolve, { once: true }); + utils.sendNativeMouseEvent( + x * window.devicePixelRatio, + y * window.devicePixelRatio, + utils.NATIVE_MOUSE_MESSAGE_MOVE, + 0, + {}, + component + ); + }); + + // Wheel event. + await new Promise(resolve => { + info("waiting for wheel"); + window.addEventListener("wheel", resolve, { once: true }); + utils.sendNativeMouseScrollEvent( + x * window.devicePixelRatio, + y * window.devicePixelRatio, + // nativeVerticalWheelEventMsg is defined in apz_test_native_event_utils.js + // eslint-disable-next-line no-undef + nativeVerticalWheelEventMsg(), + 0, + // nativeScrollUnits is defined in apz_test_native_event_utils.js + // eslint-disable-next-line no-undef + -nativeScrollUnits(component, scrollDelta), + 0, + 0, + 0, + component + ); + }); + + info("waiting for apz"); + // promiseApzFlushedRepaints is defined in apz_test_utils.js + // eslint-disable-next-line no-undef + await promiseApzFlushedRepaints(); +} + +function synthesizeColumnDrag( + aTree, + aMouseDownColumnNumber, + aMouseUpColumnNumber, + aAfter +) { + var columns = getSortedColumnArray(aTree); + + var down = columns[aMouseDownColumnNumber].element; + var up = columns[aMouseUpColumnNumber].element; + + // Target the initial mousedown in the middle of the column header so we + // avoid the extra hit test space given to the splitter + var columnWidth = down.getBoundingClientRect().width; + var splitterHitWidth = columnWidth / 2; + synthesizeMouse(down, splitterHitWidth, 3, { type: "mousedown" }); + + var offsetX = 0; + if (aAfter) { + offsetX = columnWidth; + } + + if (aMouseUpColumnNumber > aMouseDownColumnNumber) { + for (let i = aMouseDownColumnNumber; i <= aMouseUpColumnNumber; i++) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } else { + for (let i = aMouseDownColumnNumber; i >= aMouseUpColumnNumber; i--) { + let move = columns[i].element; + synthesizeMouse(move, offsetX, 3, { type: "mousemove" }); + } + } + + synthesizeMouse(up, offsetX, 3, { type: "mouseup" }); +} + +function arrayMove(aArray, aFrom, aTo, aAfter) { + var o = aArray.splice(aFrom, 1)[0]; + if (aTo > aFrom) { + aTo--; + } + + if (aAfter) { + aTo++; + } + + aArray.splice(aTo, 0, o); +} + +function getSortedColumnArray(aTree) { + var columns = aTree.columns; + var array = []; + for (let i = 0; i < columns.length; i++) { + array.push(columns.getColumnAt(i)); + } + + array.sort(function (a, b) { + var o1 = parseInt(a.element.style.order); + var o2 = parseInt(b.element.style.order); + return o1 - o2; + }); + return array; +} + +function checkColumns(aTree, aReference, aMessage) { + var columns = getSortedColumnArray(aTree); + var ids = []; + columns.forEach(function (e) { + ids.push(e.element.id); + }); + is(compareArrays(ids, aReference), true, aMessage); +} + +function mouseOnCell(tree, row, column, testname) { + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouseExpectEvent( + tree.body, + rect.x, + rect.y, + {}, + tree, + "select", + testname + ); +} + +function mouseClickOnColumnHeader( + aColumns, + aColumnIndex, + aButton, + aClickCount +) { + var columnHeader = aColumns[aColumnIndex].element; + var columnHeaderRect = columnHeader.getBoundingClientRect(); + var columnWidth = columnHeaderRect.right - columnHeaderRect.left; + // For multiple click we send separate click events, with increasing + // clickCount. This simulates the common behavior of multiple clicks. + for (let i = 1; i <= aClickCount; i++) { + // Target the middle of the column header. + synthesizeMouse(columnHeader, columnWidth / 2, 3, { + button: aButton, + clickCount: i, + }); + } +} + +function mouseDblClickOnCell(tree, row, column, testname) { + // select the row we will edit + var selection = tree.view.selection; + selection.select(row); + tree.ensureRowIsVisible(row); + + // get cell coordinates + var rect = tree.getCoordsForCellItem(row, column, "text"); + + synthesizeMouse(tree.body, rect.x, rect.y, { clickCount: 2 }); +} + +function compareArrays(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + + for (let i = 0; i < arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + + return true; +} + +function convertDOMtoTreeRowInfo(treechildren, level, rowidx) { + var obj = { rows: [] }; + + var parentidx = rowidx.value; + + treechildren = treechildren.childNodes; + for (var r = 0; r < treechildren.length; r++) { + rowidx.value++; + + var treeitem = treechildren[r]; + if (treeitem.hasChildNodes()) { + var treerow = treeitem.firstChild; + var cellInfo = []; + for (var c = 0; c < treerow.childNodes.length; c++) { + var cell = treerow.childNodes[c]; + cellInfo.push({ + label: "" + cell.getAttribute("label"), + value: cell.getAttribute("value"), + properties: cell.getAttribute("properties"), + editable: cell.getAttribute("editable") != "false", + selectable: cell.getAttribute("selectable") != "false", + image: cell.getAttribute("src"), + mode: cell.hasAttribute("mode") + ? parseInt(cell.getAttribute("mode")) + : 3, + }); + } + + var descendants = treeitem.lastChild; + var children = + treerow == descendants + ? null + : convertDOMtoTreeRowInfo(descendants, level + 1, rowidx); + obj.rows.push({ + cells: cellInfo, + properties: treerow.getAttribute("properties"), + container: treeitem.getAttribute("container") == "true", + separator: treeitem.localName == "treeseparator", + children, + level, + parent: parentidx, + }); + } + } + + return obj; +} diff --git a/toolkit/content/tests/widgets/video.ogg b/toolkit/content/tests/widgets/video.ogg Binary files differnew file mode 100644 index 0000000000..ac7ece3519 --- /dev/null +++ b/toolkit/content/tests/widgets/video.ogg diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html new file mode 100644 index 0000000000..1f7e76a7d0 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1a.html b/toolkit/content/tests/widgets/videocontrols_direction-1a.html new file mode 100644 index 0000000000..a4d3546294 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1b.html b/toolkit/content/tests/widgets/videocontrols_direction-1b.html new file mode 100644 index 0000000000..a14b11d5ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1c.html b/toolkit/content/tests/widgets/videocontrols_direction-1c.html new file mode 100644 index 0000000000..0885ebd893 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<video controls preload="none" id="av" source="audio.wav"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1d.html b/toolkit/content/tests/widgets/videocontrols_direction-1d.html new file mode 100644 index 0000000000..a39accec72 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" dir="rtl"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-1e.html b/toolkit/content/tests/widgets/videocontrols_direction-1e.html new file mode 100644 index 0000000000..25e7c2c1f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-1e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<video controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></video> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html new file mode 100644 index 0000000000..630177883c --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2a.html b/toolkit/content/tests/widgets/videocontrols_direction-2a.html new file mode 100644 index 0000000000..2e40cdc1a7 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2a.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html dir="rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2b.html b/toolkit/content/tests/widgets/videocontrols_direction-2b.html new file mode 100644 index 0000000000..2e4dadb6ff --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2b.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html style="direction: rtl"> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2c.html b/toolkit/content/tests/widgets/videocontrols_direction-2c.html new file mode 100644 index 0000000000..a43b03e8f9 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2c.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="direction: rtl"> +<audio controls preload="none" id="av" source="audio.wav"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2d.html b/toolkit/content/tests/widgets/videocontrols_direction-2d.html new file mode 100644 index 0000000000..52d56f1ccd --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2d.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" dir="rtl"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction-2e.html b/toolkit/content/tests/widgets/videocontrols_direction-2e.html new file mode 100644 index 0000000000..58bc30e2b3 --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction-2e.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" type="text/css" href="videomask.css"> +</head> +<body style="text-align: right;"> +<audio controls preload="none" id="av" source="audio.wav" style="direction: rtl;"></audio> +<div id="mask"></div> +</body> +</html> diff --git a/toolkit/content/tests/widgets/videocontrols_direction_test.js b/toolkit/content/tests/widgets/videocontrols_direction_test.js new file mode 100644 index 0000000000..e937f06b3f --- /dev/null +++ b/toolkit/content/tests/widgets/videocontrols_direction_test.js @@ -0,0 +1,119 @@ +// This file expects `tests` to have been declared in the global scope. +/* global tests */ + +var RemoteCanvas = function (url, id) { + this.url = url; + this.id = id; + this.snapshot = null; +}; + +RemoteCanvas.CANVAS_WIDTH = 200; +RemoteCanvas.CANVAS_HEIGHT = 200; + +RemoteCanvas.prototype.compare = function (otherCanvas, expected) { + return compareSnapshots(this.snapshot, otherCanvas.snapshot, expected)[0]; +}; + +RemoteCanvas.prototype.load = function (callback) { + var iframe = document.createElement("iframe"); + iframe.id = this.id + "-iframe"; + iframe.width = RemoteCanvas.CANVAS_WIDTH + "px"; + iframe.height = RemoteCanvas.CANVAS_HEIGHT + "px"; + iframe.src = this.url; + var me = this; + iframe.addEventListener("load", function () { + info("iframe loaded"); + var m = iframe.contentDocument.getElementById("av"); + m.addEventListener( + "suspend", + function (aEvent) { + setTimeout(function () { + let mediaElement = + iframe.contentDocument.querySelector("audio, video"); + const { widget } = SpecialPowers.wrap(iframe.contentWindow) + .windowGlobalChild.getActor("UAWidgets") + .widgets.get(mediaElement); + widget.impl.Utils.l10n.translateRoots().then(() => { + me.remotePageLoaded(callback); + }); + }, 0); + }, + { once: true } + ); + m.src = m.getAttribute("source"); + }); + window.document.body.appendChild(iframe); +}; + +RemoteCanvas.prototype.remotePageLoaded = function (callback) { + var ldrFrame = document.getElementById(this.id + "-iframe"); + this.snapshot = snapshotWindow(ldrFrame.contentWindow); + this.snapshot.id = this.id + "-canvas"; + window.document.body.appendChild(this.snapshot); + callback(this); +}; + +RemoteCanvas.prototype.cleanup = function () { + var iframe = document.getElementById(this.id + "-iframe"); + iframe.remove(); + var canvas = document.getElementById(this.id + "-canvas"); + canvas.remove(); +}; + +function runTest(index) { + var canvases = []; + function testCallback(canvas) { + canvases.push(canvas); + + if (canvases.length == 2) { + // when both canvases are loaded + var expectedEqual = currentTest.op == "=="; + var result = canvases[0].compare(canvases[1], expectedEqual); + ok( + result, + "Rendering of reftest " + + currentTest.test + + " should " + + (expectedEqual ? "not " : "") + + "be different to the reference" + ); + + if (result) { + canvases[0].cleanup(); + canvases[1].cleanup(); + } else { + info("Snapshot of canvas 1: " + canvases[0].snapshot.toDataURL()); + info("Snapshot of canvas 2: " + canvases[1].snapshot.toDataURL()); + } + + if (index < tests.length - 1) { + runTest(index + 1); + } else { + SimpleTest.finish(); + } + } + } + + var currentTest = tests[index]; + var testCanvas = new RemoteCanvas(currentTest.test, "test-" + index); + testCanvas.load(testCallback); + + var refCanvas = new RemoteCanvas(currentTest.ref, "ref-" + index); + refCanvas.load(testCallback); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +window.addEventListener( + "load", + function () { + SpecialPowers.pushPrefEnv( + { set: [["media.cache_size", 40000]] }, + function () { + runTest(0); + } + ); + }, + true +); diff --git a/toolkit/content/tests/widgets/videomask.css b/toolkit/content/tests/widgets/videomask.css new file mode 100644 index 0000000000..066d441388 --- /dev/null +++ b/toolkit/content/tests/widgets/videomask.css @@ -0,0 +1,23 @@ +html, body { + margin: 0; + padding: 0; +} + +audio, video { + width: 140px; + height: 100px; + background-color: black; +} + +/** + * Create a mask for the video direction tests which covers up the throbber. + */ +#mask { + position: absolute; + z-index: 3; + width: 140px; + height: 72px; + background-color: green; + top: 0; + right: 0; +} diff --git a/toolkit/content/tests/widgets/window_label_checkbox.xhtml b/toolkit/content/tests/widgets/window_label_checkbox.xhtml new file mode 100644 index 0000000000..e1a8258b92 --- /dev/null +++ b/toolkit/content/tests/widgets/window_label_checkbox.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window title="Label Checkbox Tests" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox> + <label control="checkbox" value="Label" id="label"/> + <checkbox id="checkbox"/> + <label control="radio2" value="Label" id="label2"/> + <radiogroup> + <radio/> + <radio id="radio2"/> + </radiogroup> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + + let SimpleTest = opener.SimpleTest; + SimpleTest.waitForFocus(() => { + let ok = SimpleTest.ok; + let label = document.getElementById("label"); + let checkbox = document.getElementById("checkbox"); + let label2 = document.getElementById("label2"); + let radio2 = document.getElementById("radio2"); + checkbox.checked = true; + radio2.selected = false; + ok(checkbox.checked, "sanity check"); + ok(!radio2.selected, "sanity check"); + setTimeout(() => { + synthesizeMouseAtCenter(label, {}); + ok(!checkbox.checked, "Checkbox should be unchecked"); + synthesizeMouseAtCenter(label2, {}); + ok(radio2.selected, "Radio2 should be selected"); + opener.postMessage("done", "*"); + window.close(); + }, 0); + }); + +]]> +</script> + +</window> diff --git a/toolkit/content/tests/widgets/window_menubar.xhtml b/toolkit/content/tests/widgets/window_menubar.xhtml new file mode 100644 index 0000000000..c4ced844ad --- /dev/null +++ b/toolkit/content/tests/widgets/window_menubar.xhtml @@ -0,0 +1,1015 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<!-- the condition in the focus event handler is because pressing Tab + unfocuses and refocuses the window on Windows --> + +<window title="Popup Tests" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="popup_shared.js"></script> + +<hbox style="margin-left: 275px; margin-top: 275px;"> +<menubar id="menubar"> + <menu id="filemenu" label="File" accesskey="F"> + <menupopup id="filepopup"> + <menuitem id="item1" label="Open" accesskey="O"/> + <menuitem id="item2" label="Save" accesskey="S"/> + <menuitem id="item3" label="Close" accesskey="C"/> + </menupopup> + </menu> + <menu id="secretmenu" label="Secret Menu" accesskey="S" disabled="true"> + <menupopup> + <menuitem label="Secret Command" accesskey="S"/> + </menupopup> + </menu> + <menu id="editmenu" label="Edit" accesskey="E"> + <menupopup id="editpopup"> + <menuitem id="cut" label="Cut" accesskey="t" disabled="true"/> + <menuitem id="copy" label="Copy" accesskey="C"/> + <menuitem id="paste" label="Paste" accesskey="P"/> + </menupopup> + </menu> + <menu id="viewmenu" label="View" accesskey="V"> + <menupopup id="viewpopup"> + <menu id="toolbar" label="Toolbar" accesskey="T"> + <menupopup id="toolbarpopup"> + <menuitem id="navigation" label="Navigation" accesskey="N" disabled="true"/> + <menuitem label="Bookmarks" accesskey="B" disabled="true"/> + </menupopup> + </menu> + <menuitem label="Status Bar" accesskey="S"/> + <menu label="Sidebar" accesskey="d"> + <menupopup> + <menuitem label="Bookmarks" accesskey="B"/> + <menuitem label="History" accesskey="H"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="helpmenu" label="Help" accesskey="H"> + <menupopup id="helppopup" > + <label value="Unselectable"/> + <menuitem id="contents" label="Contents" accesskey="C"/> + <menuitem label="More Info" accesskey="I"/> + <menuitem id="amenu" label="A Menu" accesskey="M"/> + <menuitem label="Another Menu"/> + <menuitem id="one" label="One"/> + <menu id="only" label="Only Menu"> + <menupopup> + <menuitem label="Test Submenu"/> + </menupopup> + </menu> + <menuitem label="Second Menu"/> + <menuitem id="other" disabled="true" label="Other Menu"/> + <menuitem id="third" label="Third Menu"/> + <menuitem label="One Other Menu"/> + <label value="Unselectable"/> + <menuitem id="about" label="About" accesskey="A"/> + </menupopup> + </menu> +</menubar> +<hbox> + <description id="outside">Outside menubar</description> + <html:input id="input"/> +</hbox> +</hbox> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +async function moveMouseOver(id) { + // A single synthesized mouse move isn't always enough in some platforms. + let el = document.getElementById(id); + synthesizeMouse(el, 5, 5, { type: "mousemove" }); + await new Promise(r => setTimeout(r, 0)); + synthesizeMouse(el, 8, 8, { type: "mousemove" }); +} + +let gFilePopup; +window.opener.SimpleTest.waitForFocus(function () { + gFilePopup = document.getElementById("filepopup"); + var filemenu = document.getElementById("filemenu"); + filemenu.focus(); + is(filemenu.openedWithKey, false, "initial openedWithKey"); + startPopupTests(popupTests); +}, window); + +const kIsWindows = navigator.platform.indexOf("Win") == 0; +const kIsLinux = navigator.platform.includes("Linux"); + +// On Linux, the first menu opens when F10 is pressed, but on other platforms +// the menubar is focused but no menu is opened. This means that different events +// fire. +function pressF10Events() +{ + return kIsLinux ? + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "popupshowing filepopup", "popupshown filepopup"] : + [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ]; +} + +function closeAfterF10Events() +{ + if (kIsLinux) { + return [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ]; + } + + return [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ]; +} + +var popupTests = [ +{ + testname: "press on menu", + events: [ "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", "popupshowing filepopup", "popupshown filepopup" ], + test() { synthesizeMouse(document.getElementById("filemenu"), 8, 8, { }); }, + result (testname) { + checkActive(gFilePopup, "", testname); + checkOpen("filemenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // check that pressing cursor down while there is no selection + // highlights the first item + testname: "cursor down no selection", + events: [ "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor up wraps and highlights the last item + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item3" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item3", testname); } +}, +{ + // check that pressing cursor down wraps and highlights the first item + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive item3", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, +{ + // check that pressing cursor down highlights the second item + testname: "cursor down", + events: [ "DOMMenuItemInactive item1", "DOMMenuItemActive item2" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { checkActive(gFilePopup, "item2", testname); } +}, +{ + // check that pressing cursor up highlights the second item + testname: "cursor up", + events: [ "DOMMenuItemInactive item2", "DOMMenuItemActive item1" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { checkActive(gFilePopup, "item1", testname); } +}, + +{ + // cursor right should skip the disabled menu and move to the edit menu + testname: "cursor right skip disabled", + events() { + var elist = [ + // the file menu gets deactivated, the file menu gets hidden, then + // the edit menu is activated + "DOMMenuItemInactive filemenu", "DOMMenuItemActive editmenu", + "popuphiding filepopup", "popuphidden filepopup", + // the popupshowing event gets fired when showing the edit menu. + "popupshowing editpopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + ]; + // finally, the first item is activated and popupshown is fired. + // On Windows, don't skip disabled items. + if (kIsWindows) { + elist.push("DOMMenuItemActive cut"); + } else { + elist.push("DOMMenuItemActive copy"); + } + elist.push("popupshown editpopup"); + return elist; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + var expected = kIsWindows ? "cut" : "copy"; + checkActive(document.getElementById("editpopup"), expected, testname); + checkClosed("filemenu", testname); + checkOpen("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // on Windows, a disabled item is selected, so pressing RETURN should close + // the menu but not fire a command event + testname: "enter on disabled", + events() { + if (kIsWindows) { + return [ + "popuphiding editpopup", + "popuphidden editpopup", + "DOMMenuItemInactive cut", + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + ]; + } + return [ + "DOMMenuItemInactive copy", + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command copy", + "popuphiding editpopup", "popuphidden editpopup", + ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkClosed("editmenu", testname); + is(document.getElementById("editmenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + // pressing Alt + a key should open the corresponding menu + testname: "open with accelerator", + events() { + return [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive viewmenu", + "popupshowing viewpopup", + "DOMMenuItemActive toolbar", + "popupshown viewpopup", + ]; + }, + test() { synthesizeKey("V", { altKey: true }); }, + result(testname) { + checkOpen("viewmenu", testname); + is(document.getElementById("viewmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // open the submenu with the cursor right key + testname: "open submenu with cursor right", + events() { + // on Windows, the disabled 'navigation' item can still be highlighted + if (kIsWindows) { + return ["popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + } +}, +{ + // close the submenu with the cursor left key + testname: "close submenu with cursor left", + events() { + if (kIsWindows) { + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + ]; + } + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + ]; + }, + test() { + synthesizeKey("KEY_ArrowLeft"); + }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + } +}, +{ + // open the submenu with the enter key + testname: "open submenu with enter", + events() { + if (kIsWindows) { + // on Windows, the disabled 'navigation' item can stll be highlighted + return [ "popupshowing toolbarpopup", "DOMMenuItemActive navigation", + "popupshown toolbarpopup" ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + testname: "close submenu with escape", + events() { + if (kIsWindows) { + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + ]; + } + return [ + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "DOMMenuInactive toolbarpopup", + ]; + }, + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkClosed("toolbar", testname); + }, +}, +{ + testname: "open submenu with enter again", + condition() { return kIsWindows; }, + events() { + // on Windows, the disabled 'navigation' item can stll be highlighted + if (kIsWindows) { + return [ + "popupshowing toolbarpopup", + "DOMMenuItemActive navigation", + "popupshown toolbarpopup" + ]; + } + return [ "popupshowing toolbarpopup", "popupshown toolbarpopup" ]; + }, + test() { synthesizeKey("KEY_Enter"); }, + result(testname) { + checkOpen("viewmenu", testname); + checkOpen("toolbar", testname); + }, +}, +{ + // while a submenu is open, switch to the next toplevel menu with the cursor right key + testname: "while a submenu is open, switch to the next menu with the cursor right", + condition() { return kIsWindows; }, + events: [ + "DOMMenuItemInactive viewmenu", + "DOMMenuItemActive helpmenu", + "popuphiding toolbarpopup", + "popuphidden toolbarpopup", + "popuphiding viewpopup", + "popuphidden viewpopup", + "popupshowing helppopup", + "DOMMenuItemInactive navigation", + "DOMMenuInactive toolbarpopup", + "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", + "DOMMenuItemActive contents", + "popupshown helppopup" + ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { + checkOpen("helpmenu", testname); + checkClosed("toolbar", testname); + checkClosed("viewmenu", testname); + } +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return kIsWindows; }, + events: [ + "popuphiding helppopup", + "popuphidden helppopup", + "DOMMenuItemInactive contents", + "DOMMenuInactive helppopup", + ], + test() { synthesizeKey("KEY_Escape"); }, + result(testname) { + checkClosed("helpmenu", testname); + }, +}, +{ + // close the main menu with the escape key + testname: "close menubar menu with escape", + condition() { return !kIsWindows; }, + events: [ + "popuphiding viewpopup", + "popuphidden viewpopup", + "DOMMenuItemInactive toolbar", + "DOMMenuInactive viewpopup", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + checkClosed("viewmenu", testname); + }, +}, +{ + // Deactivate menubar with the escape key. + testname: "deactivate menubar menu with escape", + events: [ + "DOMMenuItemInactive " + (kIsWindows ? "helpmenu" : "viewmenu"), + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeKey("KEY_Escape"); + }, + result(testname) { + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keydown event is consumed. + testname: "alt shouldn't activate menubar if keydown event is consumed", + test() { + document.addEventListener("keydown", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it, + // but it should be ignored if the alt keyup event is consumed. + testname: "alt shouldn't activate menubar if keyup event is consumed", + test() { + document.addEventListener("keyup", function (aEvent) { + aEvent.preventDefault(); + }, {once: true}); + synthesizeKey("KEY_Alt"); + }, + result(testname) { + ok(!document.getElementById("filemenu").openedWithKey, testname); + checkClosed("filemenu", testname); + }, +}, +{ + // Pressing Alt should highlight the first menu but not open it. + testname: "alt to activate menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + checkClosed("filemenu", testname); + }, +}, +{ + // pressing cursor left should select the previous menu but not open it + testname: "cursor left on active menubar", + events: [ "DOMMenuItemInactive filemenu", "DOMMenuItemActive helpmenu" ], + test() { synthesizeKey("KEY_ArrowLeft"); }, + result(testname) { checkClosed("helpmenu", testname); }, +}, +{ + // pressing cursor right should select the previous menu but not open it + testname: "cursor right on active menubar", + events: [ "DOMMenuItemInactive helpmenu", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_ArrowRight"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing a character should act as an accelerator and open the menu + testname: "accelerator on active menubar", + events: [ + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + "popupshowing helppopup", + "DOMMenuItemActive contents", + "popupshown helppopup", + ], + test() { sendChar("h"); }, + result(testname) { + checkOpen("helpmenu", testname); + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + }, +}, +{ + // check that pressing cursor up skips non menuitems + testname: "cursor up wrap", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive about" ], + test() { synthesizeKey("KEY_ArrowUp"); }, + result(testname) { } +}, +{ + // check that pressing cursor down skips non menuitems + testname: "cursor down wrap", + events: [ "DOMMenuItemInactive about", "DOMMenuItemActive contents" ], + test() { synthesizeKey("KEY_ArrowDown"); }, + result(testname) { } +}, +{ + // check that pressing a menuitem's accelerator selects it + testname: "menuitem accelerator", + events: [ + "DOMMenuItemInactive contents", + "DOMMenuItemActive amenu", + "DOMMenuItemInactive amenu", + "DOMMenuInactive helppopup", + "DOMMenuItemInactive helpmenu", + "DOMMenuBarInactive menubar", + "command amenu", + "popuphiding helppopup", + "popuphidden helppopup", + ], + test() { sendChar("m"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu. On Linux, the menu is opened. + testname: "F10 to activate menubar", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, true, testname + " openedWithKey"); + if (kIsLinux) { + checkOpen("filemenu", testname); + } else { + checkClosed("filemenu", testname); + } + }, +}, +{ + // pressing cursor left then down should open a menu + testname: "cursor down on menu", + events: kIsLinux ? + [ + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "popuphiding filepopup", + "popuphidden filepopup", + "popupshowing helppopup", + "DOMMenuInactive filepopup", + "popupshown helppopup", + "DOMMenuItemActive contents", + ] : [ + "popupshowing helppopup", + "DOMMenuItemInactive filemenu", + "DOMMenuItemActive helpmenu", + // This is in a different order than the + // "accelerator on active menubar" because menus opened from a + // shortcut key are fired asynchronously + "DOMMenuItemActive contents", + "popupshown helppopup", + ], + async test() { + if (kIsLinux) { + // On linux we need to wait so that the hiding of the file popup happens + // (and the help popup opens) to send the key down. + let helpPopupShown = new Promise(r => { + document.getElementById("helppopup").addEventListener("popupshown", r, { once: true }); + }); + synthesizeKey("KEY_ArrowLeft"); + await helpPopupShown; + synthesizeKey("KEY_ArrowDown"); + } else { + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowDown"); + } + }, + result(testname) { + is(document.getElementById("helpmenu").openedWithKey, true, testname + " openedWithKey"); + } +}, +{ + // pressing a letter that doesn't correspond to an accelerator. The menu + // should not close because there is more than one item corresponding to + // that letter + testname: "menuitem with no accelerator", + events: [ "DOMMenuItemInactive contents", "DOMMenuItemActive one" ], + test() { sendChar("o"); }, + result(testname) { checkOpen("helpmenu", testname); } +}, +{ + // pressing the letter again should select the next one that starts with + // that letter + testname: "menuitem with no accelerator again", + events: [ "DOMMenuItemInactive one", "DOMMenuItemActive only" ], + test() { sendChar("o"); }, + result(testname) { + // 'only' is a menu but it should not be open + checkOpen("helpmenu", testname); + checkClosed("only", testname); + } +}, +{ + // pressing the letter again when the next item is disabled should still + // select the disabled item + testname: "menuitem with no accelerator disabled", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive only", "DOMMenuItemActive other" ], + test() { sendChar("o"); }, + result(testname) { } +}, +{ + // when only one menuitem starting with that letter exists, it should be + // selected and the menu closed + testname: "menuitem with no accelerator single", + events() { + let elist = [ + "DOMMenuItemInactive other", + "DOMMenuItemActive third", + "DOMMenuItemInactive third", + "DOMMenuInactive helppopup", + "DOMMenuItemInactive helpmenu", + "DOMMenuBarInactive menubar", + "command third", + "popuphiding helppopup", + "popuphidden helppopup" + ]; + if (!kIsWindows) { + elist[0] = "DOMMenuItemInactive only"; + } + return elist; + }, + test() { sendChar("t"); }, + result(testname) { checkClosed("helpmenu", testname); } +}, +{ + // pressing F10 should highlight the first menu but not open it + testname: "F10 to activate menubar again", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, + result(testname) { checkClosed("filemenu", testname); }, +}, +{ + // pressing an accelerator for a disabled item should deactivate the menubar + testname: "accelerator for disabled menu", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { sendChar("s"); }, + result(testname) { + checkClosed("secretmenu", testname); + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + }, +}, +{ + testname: "press on disabled menu", + test() { + synthesizeMouse(document.getElementById("secretmenu"), 8, 8, { }); + }, + result (testname) { + checkClosed("secretmenu", testname); + } +}, +{ + testname: "press on second menu with shift", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", + "popupshowing editpopup", + "popupshown editpopup", + ], + test() { + synthesizeMouse(document.getElementById("editmenu"), 8, 8, { shiftKey : true }); + }, + result (testname) { + checkOpen("editmenu", testname); + checkActive(document.getElementById("menubar"), "editmenu", testname); + } +}, +{ + testname: "press on disabled menuitem", + test() { + synthesizeMouse(document.getElementById("cut"), 8, 8, { }); + }, + result (testname) { + checkOpen("editmenu", testname); + } +}, +{ + testname: "press on menuitem", + events: [ + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command copy", + "popuphiding editpopup", + "popuphidden editpopup", + ], + test() { + synthesizeMouse(document.getElementById("copy"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + // this test ensures that the menu can still be opened by clicking after selecting + // a menuitem from the menu. See bug 399350. + testname: "press on menu after menuitem selected", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive editmenu", + "popupshowing editpopup", + "popupshown editpopup", + ], + test() { synthesizeMouse(document.getElementById("editmenu"), 8, 8, { }); }, + result (testname) { + checkActive(document.getElementById("editpopup"), "", testname); + checkOpen("editmenu", testname); + } +}, +{ // try selecting a different command + testname: "press on menuitem again", + events: [ + "DOMMenuInactive editpopup", + "DOMMenuItemInactive editmenu", + "DOMMenuBarInactive menubar", + "command paste", + "popuphiding editpopup", + "popuphidden editpopup", + ], + test() { + synthesizeMouse(document.getElementById("paste"), 8, 8, { }); + }, + result (testname) { + checkClosed("editmenu", testname); + } +}, +{ + testname: "F10 to activate menubar for tab deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with tab key", + events: closeAfterF10Events(), + test() { synthesizeKey("KEY_Tab"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for escape deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with escape key", + events: closeAfterF10Events(), + test() { + synthesizeKey("KEY_Escape"); + if (kIsLinux) { + // One to close the menu, one to deactivate the menubar. + synthesizeKey("KEY_Escape"); + } + }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for f10 deactivation", + events: pressF10Events(), + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with f10 key", + events: closeAfterF10Events(), + test() { synthesizeKey("KEY_F10"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "F10 to activate menubar for alt deactivation", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { synthesizeKey("KEY_F10"); }, +}, +{ + testname: "Deactivate menubar with alt key", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { synthesizeKey("KEY_Alt"); }, + result(testname) { + is(document.getElementById("filemenu").openedWithKey, false, testname + " openedWithKey"); + } +}, +{ + testname: "Don't activate menubar with mousedown during alt key auto-repeat", + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mousedown", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeMouse(document.getElementById("menubar"), 8, -30, { type: "mouseup", altKey: true }); + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + }, + result (testname) { + checkActive(document.getElementById("menubar"), "", testname); + } +}, + +{ + testname: "Open menu and press alt key by itself - open menu", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + "popupshowing filepopup", + "DOMMenuItemActive item1", + "popupshown filepopup", + ], + test() { synthesizeKey("F", { altKey: true }); }, + result (testname) { + checkOpen("filemenu", testname); + } +}, +{ + testname: "Open menu and press alt key by itself - close menu", + events: [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, + +// Following 4 tests are a test of bug 616797, don't insert any new tests +// between them. +{ + testname: "Open file menu by accelerator", + condition() { return kIsWindows; }, + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + "popupshowing filepopup", + "DOMMenuItemActive item1", + "popupshown filepopup" + ], + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("f", {altKey: true}); + synthesizeKey("KEY_Alt", {type: "keyup"}); + } +}, +{ + testname: "Close file menu by click at outside of popup menu", + condition() { return kIsWindows; }, + events: [ + "popuphiding filepopup", + "popuphidden filepopup", + "DOMMenuItemInactive item1", + "DOMMenuInactive filepopup", + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + document.getElementById("filepopup").hidePopup(); + } +}, +{ + testname: "Alt keydown set focus the menubar", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { + synthesizeKey("KEY_Alt"); + }, + result (testname) { + checkClosed("filemenu", testname); + } +}, +{ + testname: "unset focus the menubar", + condition() { return kIsWindows; }, + events: [ "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + test() { + synthesizeKey("KEY_Alt"); + } +}, + +// bug 1811466 +{ + testname: "Menubar activation / deactivation on mouse over", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + async test() { + await moveMouseOver("filemenu"); + await moveMouseOver("outside"); + }, +}, + +// bug 1811466 +{ + testname: "Menubar hover in and out after key activation (part 1)", + events: [ + "DOMMenuBarActive menubar", + "DOMMenuItemActive filemenu", + /* Shouldn't deactivate the menubar nor filemenu! */ + ], + async test() { + synthesizeKey("KEY_Alt"); + await moveMouseOver("filemenu"); + await moveMouseOver("outside"); + }, +}, +{ + testname: "Deactivate the menubar by mouse", + events: [ + "DOMMenuItemInactive filemenu", + "DOMMenuBarInactive menubar", + ], + test() { + synthesizeMouse(document.getElementById("outside"), 8, 8, {}); + }, +}, + + +// bug 1818241 +{ + testname: "Shortcut navigation on mouse-activated menubar", + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu", "DOMMenuItemInactive filemenu", "DOMMenuBarInactive menubar" ], + async test() { + let input = document.getElementById("input"); + input.value = ""; + input.focus(); + await moveMouseOver("filemenu"); + synthesizeKey("F"); + await moveMouseOver("outside"); + is(input.value, "F", "Key shouldn't be consumed by menubar"); + }, +}, + +// FIXME: This leaves the menubar in a state where IsActive() is false but +// IsActiveByKeyboard() is true! +{ + testname: "Trying to activate menubar without activatable items shouldn't crash", + events: [ "TestDone menubar" ], + test() { + const items = document.querySelectorAll("menubar > menu"); + let wasDisabled = {}; + for (let item of items) { + wasDisabled[item] = item.disabled; + item.disabled = true; + } + + synthesizeKey("KEY_F10"); + setTimeout(function() { + synthesizeKey("KEY_F10"); + + for (let item of items) { + item.disabled = wasDisabled[item]; + } + + document.getElementById("menubar").dispatchEvent(new CustomEvent("TestDone", { bubbles: true })); + }, 0); + } +}, + +// bug 625151 +{ + testname: "Alt key state before deactivating the window shouldn't prevent " + + "next Alt key handling", + condition() { return kIsWindows; }, + events: [ "DOMMenuBarActive menubar", "DOMMenuItemActive filemenu" ], + test() { + synthesizeKey("KEY_Alt", {type: "keydown"}); + synthesizeKey("KEY_Tab", {type: "keydown"}); // cancels the Alt key + var thisWindow = window; + var newWindow = + window.open("javascript:'<html><body>dummy</body></html>';", "_blank", "width=100,height=100"); + newWindow.addEventListener("focus", function () { + thisWindow.addEventListener("focus", function () { + setTimeout(function () { + synthesizeKey("KEY_Alt", {}, thisWindow); + }, 0); + }, {once: true}); + newWindow.close(); + thisWindow.focus(); + }, {once: true}); + } +}, + +]; + +]]> +</script> + +</window> |