From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- toolkit/modules/AppConstants.jsm | 343 +++++ toolkit/modules/AsyncPrefs.jsm | 183 +++ toolkit/modules/Battery.jsm | 73 + toolkit/modules/BinarySearch.jsm | 75 + toolkit/modules/BrowserUtils.jsm | 586 +++++++ toolkit/modules/CanonicalJSON.jsm | 62 + toolkit/modules/CertUtils.jsm | 222 +++ toolkit/modules/CharsetMenu.jsm | 267 ++++ toolkit/modules/ClientID.jsm | 231 +++ toolkit/modules/Color.jsm | 85 ++ toolkit/modules/Console.jsm | 713 +++++++++ toolkit/modules/DateTimePickerHelper.jsm | 174 +++ toolkit/modules/DeferredTask.jsm | 301 ++++ toolkit/modules/Deprecated.jsm | 81 + toolkit/modules/FileUtils.jsm | 176 +++ toolkit/modules/Finder.jsm | 639 ++++++++ toolkit/modules/FinderHighlighter.jsm | 1615 ++++++++++++++++++++ toolkit/modules/FinderIterator.jsm | 657 ++++++++ toolkit/modules/FormLikeFactory.jsm | 166 ++ toolkit/modules/GMPInstallManager.jsm | 523 +++++++ toolkit/modules/GMPUtils.jsm | 208 +++ toolkit/modules/Geometry.jsm | 334 ++++ toolkit/modules/Http.jsm | 100 ++ toolkit/modules/InlineSpellChecker.jsm | 593 +++++++ toolkit/modules/InlineSpellCheckerContent.jsm | 141 ++ toolkit/modules/Integration.jsm | 283 ++++ toolkit/modules/JSONFile.jsm | 266 ++++ toolkit/modules/LightweightThemeConsumer.jsm | 180 +++ toolkit/modules/LoadContextInfo.jsm | 15 + toolkit/modules/Locale.jsm | 93 ++ toolkit/modules/Log.jsm | 969 ++++++++++++ toolkit/modules/Memory.jsm | 77 + toolkit/modules/NLP.jsm | 76 + toolkit/modules/NewTabUtils.jsm | 1488 ++++++++++++++++++ toolkit/modules/ObjectUtils.jsm | 176 +++ toolkit/modules/PageMenu.jsm | 320 ++++ toolkit/modules/PageMetadata.jsm | 297 ++++ toolkit/modules/PermissionsUtils.jsm | 99 ++ toolkit/modules/PopupNotifications.jsm | 1337 ++++++++++++++++ toolkit/modules/Preferences.jsm | 428 ++++++ toolkit/modules/PrivateBrowsingUtils.jsm | 103 ++ toolkit/modules/ProfileAge.jsm | 205 +++ toolkit/modules/Promise-backend.js | 970 ++++++++++++ toolkit/modules/Promise.jsm | 101 ++ toolkit/modules/PromiseMessage.jsm | 33 + toolkit/modules/PromiseUtils.jsm | 53 + toolkit/modules/PropertyListUtils.jsm | 820 ++++++++++ toolkit/modules/RemoteController.jsm | 96 ++ toolkit/modules/RemoteFinder.jsm | 332 ++++ toolkit/modules/RemotePageManager.jsm | 534 +++++++ toolkit/modules/RemoteSecurityUI.jsm | 34 + toolkit/modules/RemoteWebProgress.jsm | 285 ++++ toolkit/modules/ResetProfile.jsm | 65 + toolkit/modules/ResponsivenessMonitor.jsm | 37 + toolkit/modules/SelectContentHelper.jsm | 246 +++ toolkit/modules/SelectParentHelper.jsm | 211 +++ toolkit/modules/ServiceRequest.jsm | 49 + toolkit/modules/Services.jsm | 117 ++ toolkit/modules/SessionRecorder.jsm | 403 +++++ toolkit/modules/ShortcutUtils.jsm | 113 ++ toolkit/modules/Sntp.jsm | 334 ++++ toolkit/modules/SpatialNavigation.jsm | 606 ++++++++ toolkit/modules/Sqlite.jsm | 1461 ++++++++++++++++++ toolkit/modules/Task.jsm | 527 +++++++ toolkit/modules/Timer.jsm | 54 + toolkit/modules/Troubleshoot.jsm | 589 +++++++ toolkit/modules/UpdateUtils.jsm | 392 +++++ toolkit/modules/WebChannel.jsm | 328 ++++ toolkit/modules/WindowDraggingUtils.jsm | 99 ++ toolkit/modules/WindowsRegistry.jsm | 90 ++ toolkit/modules/ZipUtils.jsm | 223 +++ toolkit/modules/addons/.eslintrc.js | 15 + toolkit/modules/addons/MatchPattern.jsm | 352 +++++ toolkit/modules/addons/WebNavigation.jsm | 370 +++++ toolkit/modules/addons/WebNavigationContent.js | 272 ++++ toolkit/modules/addons/WebNavigationFrames.jsm | 142 ++ toolkit/modules/addons/WebRequest.jsm | 918 +++++++++++ toolkit/modules/addons/WebRequestCommon.jsm | 57 + toolkit/modules/addons/WebRequestContent.js | 192 +++ toolkit/modules/addons/WebRequestUpload.jsm | 321 ++++ toolkit/modules/debug.js | 79 + toolkit/modules/docs/AsyncShutdown.rst | 264 ++++ toolkit/modules/docs/index.rst | 10 + toolkit/modules/moz.build | 157 ++ toolkit/modules/secondscreen/PresentationApp.jsm | 190 +++ toolkit/modules/secondscreen/RokuApp.jsm | 230 +++ .../secondscreen/SimpleServiceDiscovery.jsm | 435 ++++++ toolkit/modules/sessionstore/FormData.jsm | 412 +++++ toolkit/modules/sessionstore/ScrollPosition.jsm | 103 ++ toolkit/modules/sessionstore/Utils.jsm | 107 ++ toolkit/modules/sessionstore/XPathGenerator.jsm | 119 ++ toolkit/modules/subprocess/.eslintrc.js | 28 + toolkit/modules/subprocess/Subprocess.jsm | 163 ++ toolkit/modules/subprocess/docs/index.rst | 227 +++ toolkit/modules/subprocess/moz.build | 32 + toolkit/modules/subprocess/subprocess_common.jsm | 703 +++++++++ toolkit/modules/subprocess/subprocess_shared.js | 98 ++ .../modules/subprocess/subprocess_shared_unix.js | 157 ++ .../modules/subprocess/subprocess_shared_win.js | 522 +++++++ toolkit/modules/subprocess/subprocess_unix.jsm | 166 ++ toolkit/modules/subprocess/subprocess_win.jsm | 168 ++ .../modules/subprocess/subprocess_worker_common.js | 229 +++ .../modules/subprocess/subprocess_worker_unix.js | 609 ++++++++ .../modules/subprocess/subprocess_worker_win.js | 708 +++++++++ .../modules/subprocess/test/xpcshell/.eslintrc.js | 5 + .../subprocess/test/xpcshell/data_test_script.py | 55 + .../subprocess/test/xpcshell/data_text_file.txt | 0 toolkit/modules/subprocess/test/xpcshell/head.js | 14 + .../subprocess/test/xpcshell/test_subprocess.js | 769 ++++++++++ .../xpcshell/test_subprocess_getEnvironment.js | 17 + .../test/xpcshell/test_subprocess_pathSearch.js | 73 + .../modules/subprocess/test/xpcshell/xpcshell.ini | 14 + toolkit/modules/tests/MockDocument.jsm | 50 + toolkit/modules/tests/PromiseTestUtils.jsm | 241 +++ toolkit/modules/tests/browser/.eslintrc.js | 7 + .../modules/tests/browser/WebRequest_dynamic.sjs | 13 + .../tests/browser/WebRequest_redirection.sjs | 4 + toolkit/modules/tests/browser/browser.ini | 41 + .../modules/tests/browser/browser_AsyncPrefs.js | 97 ++ toolkit/modules/tests/browser/browser_Battery.js | 51 + .../modules/tests/browser/browser_Deprecated.js | 157 ++ toolkit/modules/tests/browser/browser_Finder.js | 62 + .../tests/browser/browser_FinderHighlighter.js | 460 ++++++ .../browser/browser_Finder_hidden_textarea.js | 52 + toolkit/modules/tests/browser/browser_Geometry.js | 111 ++ .../tests/browser/browser_InlineSpellChecker.js | 121 ++ .../modules/tests/browser/browser_PageMetadata.js | 73 + .../tests/browser/browser_PromiseMessage.js | 38 + .../tests/browser/browser_RemotePageManager.js | 400 +++++ .../modules/tests/browser/browser_Troubleshoot.js | 546 +++++++ .../modules/tests/browser/browser_WebNavigation.js | 140 ++ .../modules/tests/browser/browser_WebRequest.js | 214 +++ .../tests/browser/browser_WebRequest_cookies.js | 89 ++ .../tests/browser/browser_WebRequest_filtering.js | 118 ++ toolkit/modules/tests/browser/dummy_page.html | 7 + .../modules/tests/browser/file_FinderSample.html | 824 ++++++++++ .../tests/browser/file_WebNavigation_page1.html | 9 + .../tests/browser/file_WebNavigation_page2.html | 7 + .../tests/browser/file_WebNavigation_page3.html | 9 + .../tests/browser/file_WebRequest_page1.html | 29 + .../tests/browser/file_WebRequest_page2.html | 25 + toolkit/modules/tests/browser/file_image_bad.png | Bin 0 -> 5401 bytes toolkit/modules/tests/browser/file_image_good.png | Bin 0 -> 580 bytes .../modules/tests/browser/file_image_redirect.png | Bin 0 -> 5401 bytes toolkit/modules/tests/browser/file_script_bad.js | 1 + toolkit/modules/tests/browser/file_script_good.js | 1 + .../modules/tests/browser/file_script_redirect.js | 2 + toolkit/modules/tests/browser/file_script_xhr.js | 3 + toolkit/modules/tests/browser/file_style_bad.css | 3 + toolkit/modules/tests/browser/file_style_good.css | 3 + .../modules/tests/browser/file_style_redirect.css | 3 + toolkit/modules/tests/browser/head.js | 23 + toolkit/modules/tests/browser/metadata_simple.html | 10 + toolkit/modules/tests/browser/metadata_titles.html | 11 + .../tests/browser/metadata_titles_fallback.html | 10 + .../tests/browser/testremotepagemanager.html | 66 + toolkit/modules/tests/chrome/.eslintrc.js | 7 + toolkit/modules/tests/chrome/chrome.ini | 3 + .../tests/chrome/test_bug544442_checkCert.xul | 155 ++ toolkit/modules/tests/mochitest/.eslintrc.js | 7 + toolkit/modules/tests/mochitest/mochitest.ini | 3 + .../tests/mochitest/test_spatial_navigation.html | 76 + toolkit/modules/tests/xpcshell/.eslintrc.js | 7 + toolkit/modules/tests/xpcshell/TestIntegration.jsm | 42 + .../modules/tests/xpcshell/chromeappsstore.sqlite | Bin 0 -> 262144 bytes .../bug710259_propertyListBinary.plist | Bin 0 -> 3277 bytes .../propertyLists/bug710259_propertyListXML.plist | 28 + .../modules/tests/xpcshell/test_BinarySearch.js | 81 + .../modules/tests/xpcshell/test_CanonicalJSON.js | 146 ++ toolkit/modules/tests/xpcshell/test_Color.js | 53 + .../modules/tests/xpcshell/test_DeferredTask.js | 390 +++++ toolkit/modules/tests/xpcshell/test_FileUtils.js | 226 +++ .../modules/tests/xpcshell/test_FinderIterator.js | 265 ++++ .../tests/xpcshell/test_GMPInstallManager.js | 794 ++++++++++ toolkit/modules/tests/xpcshell/test_Http.js | 257 ++++ toolkit/modules/tests/xpcshell/test_Integration.js | 238 +++ toolkit/modules/tests/xpcshell/test_JSONFile.js | 242 +++ toolkit/modules/tests/xpcshell/test_Log.js | 592 +++++++ .../modules/tests/xpcshell/test_Log_stackTrace.js | 30 + toolkit/modules/tests/xpcshell/test_MatchGlobs.js | 58 + .../modules/tests/xpcshell/test_MatchPattern.js | 95 ++ .../modules/tests/xpcshell/test_MatchURLFilters.js | 396 +++++ toolkit/modules/tests/xpcshell/test_NewTabUtils.js | 378 +++++ toolkit/modules/tests/xpcshell/test_ObjectUtils.js | 96 ++ .../tests/xpcshell/test_ObjectUtils_strict.js | 29 + .../tests/xpcshell/test_PermissionsUtils.js | 85 ++ toolkit/modules/tests/xpcshell/test_Preferences.js | 378 +++++ toolkit/modules/tests/xpcshell/test_Promise.js | 1105 ++++++++++++++ .../modules/tests/xpcshell/test_PromiseUtils.js | 105 ++ toolkit/modules/tests/xpcshell/test_Services.js | 90 ++ .../xpcshell/test_UpdateUtils_updatechannel.js | 38 + .../modules/tests/xpcshell/test_UpdateUtils_url.js | 292 ++++ toolkit/modules/tests/xpcshell/test_ZipUtils.js | 79 + toolkit/modules/tests/xpcshell/test_client_id.js | 95 ++ toolkit/modules/tests/xpcshell/test_jsesc.js | 9 + .../tests/xpcshell/test_propertyListsUtils.js | 106 ++ .../modules/tests/xpcshell/test_readCertPrefs.js | 97 ++ .../tests/xpcshell/test_servicerequest_xhr.js | 25 + .../tests/xpcshell/test_session_recorder.js | 306 ++++ toolkit/modules/tests/xpcshell/test_sqlite.js | 1094 +++++++++++++ .../modules/tests/xpcshell/test_sqlite_shutdown.js | 122 ++ toolkit/modules/tests/xpcshell/test_task.js | 642 ++++++++ toolkit/modules/tests/xpcshell/test_timer.js | 57 + toolkit/modules/tests/xpcshell/test_web_channel.js | 149 ++ .../tests/xpcshell/test_web_channel_broker.js | 88 ++ toolkit/modules/tests/xpcshell/xpcshell.ini | 75 + toolkit/modules/tests/xpcshell/zips/zen.zip | Bin 0 -> 1226 bytes toolkit/modules/third_party/jsesc/README | 10 + toolkit/modules/third_party/jsesc/fx-header | 26 + toolkit/modules/third_party/jsesc/jsesc.js | 299 ++++ 210 files changed, 47861 insertions(+) create mode 100644 toolkit/modules/AppConstants.jsm create mode 100644 toolkit/modules/AsyncPrefs.jsm create mode 100644 toolkit/modules/Battery.jsm create mode 100644 toolkit/modules/BinarySearch.jsm create mode 100644 toolkit/modules/BrowserUtils.jsm create mode 100644 toolkit/modules/CanonicalJSON.jsm create mode 100644 toolkit/modules/CertUtils.jsm create mode 100644 toolkit/modules/CharsetMenu.jsm create mode 100644 toolkit/modules/ClientID.jsm create mode 100644 toolkit/modules/Color.jsm create mode 100644 toolkit/modules/Console.jsm create mode 100644 toolkit/modules/DateTimePickerHelper.jsm create mode 100644 toolkit/modules/DeferredTask.jsm create mode 100644 toolkit/modules/Deprecated.jsm create mode 100644 toolkit/modules/FileUtils.jsm create mode 100644 toolkit/modules/Finder.jsm create mode 100644 toolkit/modules/FinderHighlighter.jsm create mode 100644 toolkit/modules/FinderIterator.jsm create mode 100644 toolkit/modules/FormLikeFactory.jsm create mode 100644 toolkit/modules/GMPInstallManager.jsm create mode 100644 toolkit/modules/GMPUtils.jsm create mode 100644 toolkit/modules/Geometry.jsm create mode 100644 toolkit/modules/Http.jsm create mode 100644 toolkit/modules/InlineSpellChecker.jsm create mode 100644 toolkit/modules/InlineSpellCheckerContent.jsm create mode 100644 toolkit/modules/Integration.jsm create mode 100644 toolkit/modules/JSONFile.jsm create mode 100644 toolkit/modules/LightweightThemeConsumer.jsm create mode 100644 toolkit/modules/LoadContextInfo.jsm create mode 100644 toolkit/modules/Locale.jsm create mode 100644 toolkit/modules/Log.jsm create mode 100644 toolkit/modules/Memory.jsm create mode 100644 toolkit/modules/NLP.jsm create mode 100644 toolkit/modules/NewTabUtils.jsm create mode 100644 toolkit/modules/ObjectUtils.jsm create mode 100644 toolkit/modules/PageMenu.jsm create mode 100644 toolkit/modules/PageMetadata.jsm create mode 100644 toolkit/modules/PermissionsUtils.jsm create mode 100644 toolkit/modules/PopupNotifications.jsm create mode 100644 toolkit/modules/Preferences.jsm create mode 100644 toolkit/modules/PrivateBrowsingUtils.jsm create mode 100644 toolkit/modules/ProfileAge.jsm create mode 100644 toolkit/modules/Promise-backend.js create mode 100644 toolkit/modules/Promise.jsm create mode 100644 toolkit/modules/PromiseMessage.jsm create mode 100644 toolkit/modules/PromiseUtils.jsm create mode 100644 toolkit/modules/PropertyListUtils.jsm create mode 100644 toolkit/modules/RemoteController.jsm create mode 100644 toolkit/modules/RemoteFinder.jsm create mode 100644 toolkit/modules/RemotePageManager.jsm create mode 100644 toolkit/modules/RemoteSecurityUI.jsm create mode 100644 toolkit/modules/RemoteWebProgress.jsm create mode 100644 toolkit/modules/ResetProfile.jsm create mode 100644 toolkit/modules/ResponsivenessMonitor.jsm create mode 100644 toolkit/modules/SelectContentHelper.jsm create mode 100644 toolkit/modules/SelectParentHelper.jsm create mode 100644 toolkit/modules/ServiceRequest.jsm create mode 100644 toolkit/modules/Services.jsm create mode 100644 toolkit/modules/SessionRecorder.jsm create mode 100644 toolkit/modules/ShortcutUtils.jsm create mode 100644 toolkit/modules/Sntp.jsm create mode 100644 toolkit/modules/SpatialNavigation.jsm create mode 100644 toolkit/modules/Sqlite.jsm create mode 100644 toolkit/modules/Task.jsm create mode 100644 toolkit/modules/Timer.jsm create mode 100644 toolkit/modules/Troubleshoot.jsm create mode 100644 toolkit/modules/UpdateUtils.jsm create mode 100644 toolkit/modules/WebChannel.jsm create mode 100644 toolkit/modules/WindowDraggingUtils.jsm create mode 100644 toolkit/modules/WindowsRegistry.jsm create mode 100644 toolkit/modules/ZipUtils.jsm create mode 100644 toolkit/modules/addons/.eslintrc.js create mode 100644 toolkit/modules/addons/MatchPattern.jsm create mode 100644 toolkit/modules/addons/WebNavigation.jsm create mode 100644 toolkit/modules/addons/WebNavigationContent.js create mode 100644 toolkit/modules/addons/WebNavigationFrames.jsm create mode 100644 toolkit/modules/addons/WebRequest.jsm create mode 100644 toolkit/modules/addons/WebRequestCommon.jsm create mode 100644 toolkit/modules/addons/WebRequestContent.js create mode 100644 toolkit/modules/addons/WebRequestUpload.jsm create mode 100644 toolkit/modules/debug.js create mode 100644 toolkit/modules/docs/AsyncShutdown.rst create mode 100644 toolkit/modules/docs/index.rst create mode 100644 toolkit/modules/moz.build create mode 100644 toolkit/modules/secondscreen/PresentationApp.jsm create mode 100644 toolkit/modules/secondscreen/RokuApp.jsm create mode 100644 toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm create mode 100644 toolkit/modules/sessionstore/FormData.jsm create mode 100644 toolkit/modules/sessionstore/ScrollPosition.jsm create mode 100644 toolkit/modules/sessionstore/Utils.jsm create mode 100644 toolkit/modules/sessionstore/XPathGenerator.jsm create mode 100644 toolkit/modules/subprocess/.eslintrc.js create mode 100644 toolkit/modules/subprocess/Subprocess.jsm create mode 100644 toolkit/modules/subprocess/docs/index.rst create mode 100644 toolkit/modules/subprocess/moz.build create mode 100644 toolkit/modules/subprocess/subprocess_common.jsm create mode 100644 toolkit/modules/subprocess/subprocess_shared.js create mode 100644 toolkit/modules/subprocess/subprocess_shared_unix.js create mode 100644 toolkit/modules/subprocess/subprocess_shared_win.js create mode 100644 toolkit/modules/subprocess/subprocess_unix.jsm create mode 100644 toolkit/modules/subprocess/subprocess_win.jsm create mode 100644 toolkit/modules/subprocess/subprocess_worker_common.js create mode 100644 toolkit/modules/subprocess/subprocess_worker_unix.js create mode 100644 toolkit/modules/subprocess/subprocess_worker_win.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/.eslintrc.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/data_test_script.py create mode 100644 toolkit/modules/subprocess/test/xpcshell/data_text_file.txt create mode 100644 toolkit/modules/subprocess/test/xpcshell/head.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/test_subprocess.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/test_subprocess_getEnvironment.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/test_subprocess_pathSearch.js create mode 100644 toolkit/modules/subprocess/test/xpcshell/xpcshell.ini create mode 100644 toolkit/modules/tests/MockDocument.jsm create mode 100644 toolkit/modules/tests/PromiseTestUtils.jsm create mode 100644 toolkit/modules/tests/browser/.eslintrc.js create mode 100644 toolkit/modules/tests/browser/WebRequest_dynamic.sjs create mode 100644 toolkit/modules/tests/browser/WebRequest_redirection.sjs create mode 100644 toolkit/modules/tests/browser/browser.ini create mode 100644 toolkit/modules/tests/browser/browser_AsyncPrefs.js create mode 100644 toolkit/modules/tests/browser/browser_Battery.js create mode 100644 toolkit/modules/tests/browser/browser_Deprecated.js create mode 100644 toolkit/modules/tests/browser/browser_Finder.js create mode 100644 toolkit/modules/tests/browser/browser_FinderHighlighter.js create mode 100644 toolkit/modules/tests/browser/browser_Finder_hidden_textarea.js create mode 100644 toolkit/modules/tests/browser/browser_Geometry.js create mode 100644 toolkit/modules/tests/browser/browser_InlineSpellChecker.js create mode 100644 toolkit/modules/tests/browser/browser_PageMetadata.js create mode 100644 toolkit/modules/tests/browser/browser_PromiseMessage.js create mode 100644 toolkit/modules/tests/browser/browser_RemotePageManager.js create mode 100644 toolkit/modules/tests/browser/browser_Troubleshoot.js create mode 100644 toolkit/modules/tests/browser/browser_WebNavigation.js create mode 100644 toolkit/modules/tests/browser/browser_WebRequest.js create mode 100644 toolkit/modules/tests/browser/browser_WebRequest_cookies.js create mode 100644 toolkit/modules/tests/browser/browser_WebRequest_filtering.js create mode 100644 toolkit/modules/tests/browser/dummy_page.html create mode 100644 toolkit/modules/tests/browser/file_FinderSample.html create mode 100644 toolkit/modules/tests/browser/file_WebNavigation_page1.html create mode 100644 toolkit/modules/tests/browser/file_WebNavigation_page2.html create mode 100644 toolkit/modules/tests/browser/file_WebNavigation_page3.html create mode 100644 toolkit/modules/tests/browser/file_WebRequest_page1.html create mode 100644 toolkit/modules/tests/browser/file_WebRequest_page2.html create mode 100644 toolkit/modules/tests/browser/file_image_bad.png create mode 100644 toolkit/modules/tests/browser/file_image_good.png create mode 100644 toolkit/modules/tests/browser/file_image_redirect.png create mode 100644 toolkit/modules/tests/browser/file_script_bad.js create mode 100644 toolkit/modules/tests/browser/file_script_good.js create mode 100644 toolkit/modules/tests/browser/file_script_redirect.js create mode 100644 toolkit/modules/tests/browser/file_script_xhr.js create mode 100644 toolkit/modules/tests/browser/file_style_bad.css create mode 100644 toolkit/modules/tests/browser/file_style_good.css create mode 100644 toolkit/modules/tests/browser/file_style_redirect.css create mode 100644 toolkit/modules/tests/browser/head.js create mode 100644 toolkit/modules/tests/browser/metadata_simple.html create mode 100644 toolkit/modules/tests/browser/metadata_titles.html create mode 100644 toolkit/modules/tests/browser/metadata_titles_fallback.html create mode 100644 toolkit/modules/tests/browser/testremotepagemanager.html create mode 100644 toolkit/modules/tests/chrome/.eslintrc.js create mode 100644 toolkit/modules/tests/chrome/chrome.ini create mode 100644 toolkit/modules/tests/chrome/test_bug544442_checkCert.xul create mode 100644 toolkit/modules/tests/mochitest/.eslintrc.js create mode 100644 toolkit/modules/tests/mochitest/mochitest.ini create mode 100644 toolkit/modules/tests/mochitest/test_spatial_navigation.html create mode 100644 toolkit/modules/tests/xpcshell/.eslintrc.js create mode 100644 toolkit/modules/tests/xpcshell/TestIntegration.jsm create mode 100644 toolkit/modules/tests/xpcshell/chromeappsstore.sqlite create mode 100644 toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist create mode 100644 toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist create mode 100644 toolkit/modules/tests/xpcshell/test_BinarySearch.js create mode 100644 toolkit/modules/tests/xpcshell/test_CanonicalJSON.js create mode 100644 toolkit/modules/tests/xpcshell/test_Color.js create mode 100644 toolkit/modules/tests/xpcshell/test_DeferredTask.js create mode 100644 toolkit/modules/tests/xpcshell/test_FileUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_FinderIterator.js create mode 100644 toolkit/modules/tests/xpcshell/test_GMPInstallManager.js create mode 100644 toolkit/modules/tests/xpcshell/test_Http.js create mode 100644 toolkit/modules/tests/xpcshell/test_Integration.js create mode 100644 toolkit/modules/tests/xpcshell/test_JSONFile.js create mode 100644 toolkit/modules/tests/xpcshell/test_Log.js create mode 100644 toolkit/modules/tests/xpcshell/test_Log_stackTrace.js create mode 100644 toolkit/modules/tests/xpcshell/test_MatchGlobs.js create mode 100644 toolkit/modules/tests/xpcshell/test_MatchPattern.js create mode 100644 toolkit/modules/tests/xpcshell/test_MatchURLFilters.js create mode 100644 toolkit/modules/tests/xpcshell/test_NewTabUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_ObjectUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js create mode 100644 toolkit/modules/tests/xpcshell/test_PermissionsUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_Preferences.js create mode 100644 toolkit/modules/tests/xpcshell/test_Promise.js create mode 100644 toolkit/modules/tests/xpcshell/test_PromiseUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_Services.js create mode 100644 toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js create mode 100644 toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js create mode 100644 toolkit/modules/tests/xpcshell/test_ZipUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_client_id.js create mode 100644 toolkit/modules/tests/xpcshell/test_jsesc.js create mode 100644 toolkit/modules/tests/xpcshell/test_propertyListsUtils.js create mode 100644 toolkit/modules/tests/xpcshell/test_readCertPrefs.js create mode 100644 toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js create mode 100644 toolkit/modules/tests/xpcshell/test_session_recorder.js create mode 100644 toolkit/modules/tests/xpcshell/test_sqlite.js create mode 100644 toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js create mode 100644 toolkit/modules/tests/xpcshell/test_task.js create mode 100644 toolkit/modules/tests/xpcshell/test_timer.js create mode 100644 toolkit/modules/tests/xpcshell/test_web_channel.js create mode 100644 toolkit/modules/tests/xpcshell/test_web_channel_broker.js create mode 100644 toolkit/modules/tests/xpcshell/xpcshell.ini create mode 100644 toolkit/modules/tests/xpcshell/zips/zen.zip create mode 100644 toolkit/modules/third_party/jsesc/README create mode 100644 toolkit/modules/third_party/jsesc/fx-header create mode 100644 toolkit/modules/third_party/jsesc/jsesc.js (limited to 'toolkit/modules') diff --git a/toolkit/modules/AppConstants.jsm b/toolkit/modules/AppConstants.jsm new file mode 100644 index 000000000..7ce8e1f09 --- /dev/null +++ b/toolkit/modules/AppConstants.jsm @@ -0,0 +1,343 @@ +#filter substitution +#include @TOPOBJDIR@/source-repo.h +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); + +this.EXPORTED_SYMBOLS = ["AppConstants"]; + +// Immutable for export. +this.AppConstants = Object.freeze({ + // See this wiki page for more details about channel specific build + // defines: https://wiki.mozilla.org/Platform/Channel-specific_build_defines + NIGHTLY_BUILD: +#ifdef NIGHTLY_BUILD + true, +#else + false, +#endif + + RELEASE_OR_BETA: +#ifdef RELEASE_OR_BETA + true, +#else + false, +#endif + + ACCESSIBILITY: +#ifdef ACCESSIBILITY + true, +#else + false, +#endif + + // Official corresponds, roughly, to whether this build is performed + // on Mozilla's continuous integration infrastructure. You should + // disable developer-only functionality when this flag is set. + MOZILLA_OFFICIAL: +#ifdef MOZILLA_OFFICIAL + true, +#else + false, +#endif + + MOZ_OFFICIAL_BRANDING: +#ifdef MOZ_OFFICIAL_BRANDING + true, +#else + false, +#endif + + MOZ_DEV_EDITION: +#ifdef MOZ_DEV_EDITION + true, +#else + false, +#endif + + MOZ_SERVICES_HEALTHREPORT: +#ifdef MOZ_SERVICES_HEALTHREPORT + true, +#else + false, +#endif + + MOZ_DATA_REPORTING: +#ifdef MOZ_DATA_REPORTING + true, +#else + false, +#endif + + MOZ_SANDBOX: +#ifdef MOZ_SANDBOX + true, +#else + false, +#endif + + MOZ_CONTENT_SANDBOX: +#ifdef MOZ_CONTENT_SANDBOX + true, +#else + false, +#endif + + MOZ_TELEMETRY_REPORTING: +#ifdef MOZ_TELEMETRY_REPORTING + true, +#else + false, +#endif + + MOZ_TELEMETRY_ON_BY_DEFAULT: +#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT + true, +#else + false, +#endif + + MOZ_SERVICES_CLOUDSYNC: +#ifdef MOZ_SERVICES_CLOUDSYNC + true, +#else + false, +#endif + + MOZ_UPDATER: +#ifdef MOZ_UPDATER + true, +#else + false, +#endif + + MOZ_SWITCHBOARD: +#ifdef MOZ_SWITCHBOARD + true, +#else + false, +#endif + + MOZ_WEBRTC: +#ifdef MOZ_WEBRTC + true, +#else + false, +#endif + + MOZ_WIDGET_GTK: +#ifdef MOZ_WIDGET_GTK + true, +#else + false, +#endif + +# MOZ_B2G covers both device and desktop b2g + MOZ_B2G: +#ifdef MOZ_B2G + true, +#else + false, +#endif + + XP_UNIX: +#ifdef XP_UNIX + true, +#else + false, +#endif + +# NOTE! XP_LINUX has to go after MOZ_WIDGET_ANDROID otherwise Android +# builds will be misidentified as linux. + platform: +#ifdef MOZ_WIDGET_GTK + "linux", +#elif XP_WIN + "win", +#elif XP_MACOSX + "macosx", +#elif MOZ_WIDGET_ANDROID + "android", +#elif MOZ_WIDGET_GONK + "gonk", +#elif XP_LINUX + "linux", +#else + "other", +#endif + + isPlatformAndVersionAtLeast(platform, version) { + let platformVersion = Services.sysinfo.getProperty("version"); + return platform == this.platform && + Services.vc.compare(platformVersion, version) >= 0; + }, + + isPlatformAndVersionAtMost(platform, version) { + let platformVersion = Services.sysinfo.getProperty("version"); + return platform == this.platform && + Services.vc.compare(platformVersion, version) <= 0; + }, + + MOZ_CRASHREPORTER: +#ifdef MOZ_CRASHREPORTER + true, +#else + false, +#endif + + MOZ_VERIFY_MAR_SIGNATURE: +#ifdef MOZ_VERIFY_MAR_SIGNATURE + true, +#else + false, +#endif + + MOZ_MAINTENANCE_SERVICE: +#ifdef MOZ_MAINTENANCE_SERVICE + true, +#else + false, +#endif + + E10S_TESTING_ONLY: +#ifdef E10S_TESTING_ONLY + true, +#else + false, +#endif + + DEBUG: +#ifdef DEBUG + true, +#else + false, +#endif + + ASAN: +#ifdef MOZ_ASAN + true, +#else + false, +#endif + + MOZ_B2G_RIL: +#ifdef MOZ_B2G_RIL + true, +#else + false, +#endif + + MOZ_GRAPHENE: +#ifdef MOZ_GRAPHENE + true, +#else + false, +#endif + + MOZ_SYSTEM_NSS: +#ifdef MOZ_SYSTEM_NSS + true, +#else + false, +#endif + + MOZ_PLACES: +#ifdef MOZ_PLACES + true, +#else + false, +#endif + + MOZ_REQUIRE_SIGNING: +#ifdef MOZ_REQUIRE_SIGNING + true, +#else + false, +#endif + + MENUBAR_CAN_AUTOHIDE: +#ifdef MENUBAR_CAN_AUTOHIDE + true, +#else + false, +#endif + + CAN_DRAW_IN_TITLEBAR: +#ifdef CAN_DRAW_IN_TITLEBAR + true, +#else + false, +#endif + + MOZ_ANDROID_HISTORY: +#ifdef MOZ_ANDROID_HISTORY + true, +#else + false, +#endif + + MOZ_TOOLKIT_SEARCH: +#ifdef MOZ_TOOLKIT_SEARCH + true, +#else + false, +#endif + + MOZ_ENABLE_PROFILER_SPS: +#ifdef MOZ_ENABLE_PROFILER_SPS + true, +#else + false, +#endif + + MOZ_ANDROID_ACTIVITY_STREAM: +#ifdef MOZ_ANDROID_ACTIVITY_STREAM + true, +#else + false, +#endif + + DLL_PREFIX: "@DLL_PREFIX@", + DLL_SUFFIX: "@DLL_SUFFIX@", + + MOZ_APP_NAME: "@MOZ_APP_NAME@", + MOZ_APP_VERSION: "@MOZ_APP_VERSION@", + MOZ_APP_VERSION_DISPLAY: "@MOZ_APP_VERSION_DISPLAY@", + MOZ_BUILD_APP: "@MOZ_BUILD_APP@", + MOZ_MACBUNDLE_NAME: "@MOZ_MACBUNDLE_NAME@", + MOZ_UPDATE_CHANNEL: "@MOZ_UPDATE_CHANNEL@", + INSTALL_LOCALE: "@AB_CD@", + MOZ_WIDGET_TOOLKIT: "@MOZ_WIDGET_TOOLKIT@", + ANDROID_PACKAGE_NAME: "@ANDROID_PACKAGE_NAME@", + MOZ_B2G_VERSION: @MOZ_B2G_VERSION@, + MOZ_B2G_OS_NAME: @MOZ_B2G_OS_NAME@, + + DEBUG_JS_MODULES: "@DEBUG_JS_MODULES@", + + // URL to the hg revision this was built from (e.g. + // "https://hg.mozilla.org/mozilla-central/rev/6256ec9113c1") + // On unofficial builds, this is an empty string. +#ifndef MOZ_SOURCE_URL +#define MOZ_SOURCE_URL +#endif + SOURCE_REVISION_URL: "@MOZ_SOURCE_URL@", + + HAVE_USR_LIB64_DIR: +#ifdef HAVE_USR_LIB64_DIR + true, +#else + false, +#endif + + HAVE_SHELL_SERVICE: +#ifdef HAVE_SHELL_SERVICE + true, +#else + false, +#endif +}); diff --git a/toolkit/modules/AsyncPrefs.jsm b/toolkit/modules/AsyncPrefs.jsm new file mode 100644 index 000000000..4ad523fe4 --- /dev/null +++ b/toolkit/modules/AsyncPrefs.jsm @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["AsyncPrefs"]; + +const {interfaces: Ci, utils: Cu, classes: Cc} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +const kInChildProcess = Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + +const kAllowedPrefs = new Set([ + // NB: please leave the testing prefs at the top, and sort the rest alphabetically if you add + // anything. + "testing.allowed-prefs.some-bool-pref", + "testing.allowed-prefs.some-char-pref", + "testing.allowed-prefs.some-int-pref", + + "narrate.rate", + "narrate.voice", + + "reader.font_size", + "reader.font_type", + "reader.color_scheme", + "reader.content_width", + "reader.line_height", +]); + +const kPrefTypeMap = new Map([ + ["boolean", Services.prefs.PREF_BOOL], + ["number", Services.prefs.PREF_INT], + ["string", Services.prefs.PREF_STRING], +]); + +function maybeReturnErrorForReset(pref) { + if (!kAllowedPrefs.has(pref)) { + return `Resetting pref ${pref} from content is not allowed.`; + } + return false; +} + +function maybeReturnErrorForSet(pref, value) { + if (!kAllowedPrefs.has(pref)) { + return `Setting pref ${pref} from content is not allowed.`; + } + + let valueType = typeof value; + if (!kPrefTypeMap.has(valueType)) { + return `Can't set pref ${pref} to value of type ${valueType}.`; + } + let prefType = Services.prefs.getPrefType(pref); + if (prefType != Services.prefs.PREF_INVALID && + prefType != kPrefTypeMap.get(valueType)) { + return `Can't set pref ${pref} to a value with type ${valueType} that doesn't match the pref's type ${prefType}.`; + } + return false; +} + +var AsyncPrefs; +if (kInChildProcess) { + let gUniqueId = 0; + let gMsgMap = new Map(); + + AsyncPrefs = { + set: Task.async(function(pref, value) { + let error = maybeReturnErrorForSet(pref, value); + if (error) { + return Promise.reject(error); + } + + let msgId = ++gUniqueId; + return new Promise((resolve, reject) => { + gMsgMap.set(msgId, {resolve, reject}); + Services.cpmm.sendAsyncMessage("AsyncPrefs:SetPref", {pref, value, msgId}); + }); + }), + + reset: Task.async(function(pref) { + let error = maybeReturnErrorForReset(pref); + if (error) { + return Promise.reject(error); + } + + let msgId = ++gUniqueId; + return new Promise((resolve, reject) => { + gMsgMap.set(msgId, {resolve, reject}); + Services.cpmm.sendAsyncMessage("AsyncPrefs:ResetPref", {pref, msgId}); + }); + }), + + receiveMessage(msg) { + let promiseRef = gMsgMap.get(msg.data.msgId); + if (promiseRef) { + gMsgMap.delete(msg.data.msgId); + if (msg.data.success) { + promiseRef.resolve(); + } else { + promiseRef.reject(msg.data.message); + } + } + }, + + init() { + Services.cpmm.addMessageListener("AsyncPrefs:PrefSetFinished", this); + Services.cpmm.addMessageListener("AsyncPrefs:PrefResetFinished", this); + }, + }; +} else { + AsyncPrefs = { + methodForType: { + number: "setIntPref", + boolean: "setBoolPref", + string: "setCharPref", + }, + + set: Task.async(function(pref, value) { + let error = maybeReturnErrorForSet(pref, value); + if (error) { + return Promise.reject(error); + } + let methodToUse = this.methodForType[typeof value]; + try { + Services.prefs[methodToUse](pref, value); + return Promise.resolve(value); + } catch (ex) { + Cu.reportError(ex); + return Promise.reject(ex.message); + } + }), + + reset: Task.async(function(pref) { + let error = maybeReturnErrorForReset(pref); + if (error) { + return Promise.reject(error); + } + + try { + Services.prefs.clearUserPref(pref); + return Promise.resolve(); + } catch (ex) { + Cu.reportError(ex); + return Promise.reject(ex.message); + } + }), + + receiveMessage(msg) { + if (msg.name == "AsyncPrefs:SetPref") { + this.onPrefSet(msg); + } else { + this.onPrefReset(msg); + } + }, + + onPrefReset(msg) { + let {pref, msgId} = msg.data; + this.reset(pref).then(function() { + msg.target.sendAsyncMessage("AsyncPrefs:PrefResetFinished", {msgId, success: true}); + }, function(msg) { + msg.target.sendAsyncMessage("AsyncPrefs:PrefResetFinished", {msgId, success: false, message: msg}); + }); + }, + + onPrefSet(msg) { + let {pref, value, msgId} = msg.data; + this.set(pref, value).then(function() { + msg.target.sendAsyncMessage("AsyncPrefs:PrefSetFinished", {msgId, success: true}); + }, function(msg) { + msg.target.sendAsyncMessage("AsyncPrefs:PrefSetFinished", {msgId, success: false, message: msg}); + }); + }, + + init() { + Services.ppmm.addMessageListener("AsyncPrefs:SetPref", this); + Services.ppmm.addMessageListener("AsyncPrefs:ResetPref", this); + } + }; +} + +AsyncPrefs.init(); + diff --git a/toolkit/modules/Battery.jsm b/toolkit/modules/Battery.jsm new file mode 100644 index 000000000..69184d361 --- /dev/null +++ b/toolkit/modules/Battery.jsm @@ -0,0 +1,73 @@ +// -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 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/. + +"use strict"; + +/** This module wraps around navigator.getBattery (https://developer.mozilla.org/en-US/docs/Web/API/Navigator.getBattery). + * and provides a framework for spoofing battery values in test code. + * To spoof the battery values, set `Debugging.fake = true` after exporting this with a BackstagePass, + * after which you can spoof a property yb setting the relevant property of the BatteryManager object. + */ +this.EXPORTED_SYMBOLS = ["GetBattery", "Battery"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); + +// Load Services, for the BatteryManager API +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +// Values for the fake battery. See the documentation of Navigator.battery for the meaning of each field. +var gFakeBattery = { + charging: false, + chargingTime: 0, + dischargingTime: Infinity, + level: 1, +} + +// BackendPass-exported object for toggling spoofing +this.Debugging = { + /** + * If `false`, use the DOM Battery implementation. + * Set it to `true` if you need to fake battery values + * for testing or debugging purposes. + */ + fake: false +} + +this.GetBattery = function () { + return new Services.appShell.hiddenDOMWindow.Promise(function (resolve, reject) { + // Return fake values if spoofing is enabled, otherwise fetch the real values from the BatteryManager API + if (Debugging.fake) { + resolve(gFakeBattery); + return; + } + Services.appShell.hiddenDOMWindow.navigator.getBattery().then(resolve, reject); + }); +}; + +this.Battery = {}; + +for (let k of ["charging", "chargingTime", "dischargingTime", "level"]) { + let prop = k; + Object.defineProperty(this.Battery, prop, { + get: function() { + // Return fake value if spoofing is enabled, otherwise fetch the real value from the BatteryManager API + if (Debugging.fake) { + return gFakeBattery[prop]; + } + return Services.appShell.hiddenDOMWindow.navigator.battery[prop]; + }, + set: function(fakeSetting) { + if (!Debugging.fake) { + throw new Error("Tried to set fake battery value when battery spoofing was disabled"); + } + gFakeBattery[prop] = fakeSetting; + } + }) +} diff --git a/toolkit/modules/BinarySearch.jsm b/toolkit/modules/BinarySearch.jsm new file mode 100644 index 000000000..16bca7398 --- /dev/null +++ b/toolkit/modules/BinarySearch.jsm @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "BinarySearch", +]; + +this.BinarySearch = Object.freeze({ + + /** + * Returns the index of the given target in the given array or -1 if the + * target is not found. + * + * See search() for a description of this function's parameters. + * + * @return The index of `target` in `array` or -1 if `target` is not found. + */ + indexOf: function (comparator, array, target) { + let [found, idx] = this.search(comparator, array, target); + return found ? idx : -1; + }, + + /** + * Returns the index within the given array where the given target may be + * inserted to keep the array ordered. + * + * See search() for a description of this function's parameters. + * + * @return The index in `array` where `target` may be inserted to keep `array` + * ordered. + */ + insertionIndexOf: function (comparator, array, target) { + return this.search(comparator, array, target)[1]; + }, + + /** + * Searches for the given target in the given array. + * + * @param comparator + * A function that takes two arguments and compares them, returning a + * negative number if the first should be ordered before the second, + * zero if the first and second have the same ordering, or a positive + * number if the second should be ordered before the first. The first + * argument is always `target`, and the second argument is a value + * from the array. + * @param array + * An array whose elements are ordered by `comparator`. + * @param target + * The value to search for. + * @return An array with two elements. If `target` is found, the first + * element is true, and the second element is its index in the array. + * If `target` is not found, the first element is false, and the + * second element is the index where it may be inserted to keep the + * array ordered. + */ + search: function (comparator, array, target) { + let low = 0; + let high = array.length - 1; + while (low <= high) { + // Thanks to http://jsperf.com/code-review-1480 for this tip. + let mid = (low + high) >> 1; + let cmp = comparator(target, array[mid]); + if (cmp == 0) + return [true, mid]; + if (cmp < 0) + high = mid - 1; + else + low = mid + 1; + } + return [false, low]; + }, +}); diff --git a/toolkit/modules/BrowserUtils.jsm b/toolkit/modules/BrowserUtils.jsm new file mode 100644 index 000000000..862f9619c --- /dev/null +++ b/toolkit/modules/BrowserUtils.jsm @@ -0,0 +1,586 @@ +/* -*- mode: js; 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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "BrowserUtils" ]; + +const {interfaces: Ci, utils: Cu, classes: Cc} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +Cu.importGlobalProperties(['URL']); + +this.BrowserUtils = { + + /** + * Prints arguments separated by a space and appends a new line. + */ + dumpLn: function (...args) { + for (let a of args) + dump(a + " "); + dump("\n"); + }, + + /** + * restartApplication: Restarts the application, keeping it in + * safe mode if it is already in safe mode. + */ + restartApplication: function() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + if (cancelQuit.data) { // The quit request has been canceled. + return false; + } + // if already in safe mode restart in safe mode + if (Services.appinfo.inSafeMode) { + appStartup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + return undefined; + } + appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart); + return undefined; + }, + + /** + * urlSecurityCheck: JavaScript wrapper for checkLoadURIWithPrincipal + * and checkLoadURIStrWithPrincipal. + * If |aPrincipal| is not allowed to link to |aURL|, this function throws with + * an error message. + * + * @param aURL + * The URL a page has linked to. This could be passed either as a string + * or as a nsIURI object. + * @param aPrincipal + * The principal of the document from which aURL came. + * @param aFlags + * Flags to be passed to checkLoadURIStr. If undefined, + * nsIScriptSecurityManager.STANDARD will be passed. + */ + urlSecurityCheck: function(aURL, aPrincipal, aFlags) { + var secMan = Services.scriptSecurityManager; + if (aFlags === undefined) { + aFlags = secMan.STANDARD; + } + + try { + if (aURL instanceof Ci.nsIURI) + secMan.checkLoadURIWithPrincipal(aPrincipal, aURL, aFlags); + else + secMan.checkLoadURIStrWithPrincipal(aPrincipal, aURL, aFlags); + } catch (e) { + let principalStr = ""; + try { + principalStr = " from " + aPrincipal.URI.spec; + } + catch (e2) { } + + throw "Load of " + aURL + principalStr + " denied."; + } + }, + + /** + * Return or create a principal with the codebase of one, and the originAttributes + * of an existing principal (e.g. on a docshell, where the originAttributes ought + * not to change, that is, we should keep the userContextId, privateBrowsingId, + * etc. the same when changing the principal). + * + * @param principal + * The principal whose codebase/null/system-ness we want. + * @param existingPrincipal + * The principal whose originAttributes we want, usually the current + * principal of a docshell. + * @return an nsIPrincipal that matches the codebase/null/system-ness of the first + * param, and the originAttributes of the second. + */ + principalWithMatchingOA(principal, existingPrincipal) { + // Don't care about system principals: + if (principal.isSystemPrincipal) { + return principal; + } + + // If the originAttributes already match, just return the principal as-is. + if (existingPrincipal.originSuffix == principal.originSuffix) { + return principal; + } + + let secMan = Services.scriptSecurityManager; + if (principal.isCodebasePrincipal) { + return secMan.createCodebasePrincipal(principal.URI, existingPrincipal.originAttributes); + } + + if (principal.isNullPrincipal) { + return secMan.createNullPrincipal(existingPrincipal.originAttributes); + } + throw new Error("Can't change the originAttributes of an expanded principal!"); + }, + + /** + * Constructs a new URI, using nsIIOService. + * @param aURL The URI spec. + * @param aOriginCharset The charset of the URI. + * @param aBaseURI Base URI to resolve aURL, or null. + * @return an nsIURI object based on aURL. + */ + makeURI: function(aURL, aOriginCharset, aBaseURI) { + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); + }, + + makeFileURI: function(aFile) { + return Services.io.newFileURI(aFile); + }, + + makeURIFromCPOW: function(aCPOWURI) { + return Services.io.newURI(aCPOWURI.spec, aCPOWURI.originCharset, null); + }, + + /** + * For a given DOM element, returns its position in "screen" + * coordinates. In a content process, the coordinates returned will + * be relative to the left/top of the tab. In the chrome process, + * the coordinates are relative to the user's screen. + */ + getElementBoundingScreenRect: function(aElement) { + return this.getElementBoundingRect(aElement, true); + }, + + /** + * For a given DOM element, returns its position as an offset from the topmost + * window. In a content process, the coordinates returned will be relative to + * the left/top of the topmost content area. If aInScreenCoords is true, + * screen coordinates will be returned instead. + */ + getElementBoundingRect: function(aElement, aInScreenCoords) { + let rect = aElement.getBoundingClientRect(); + let win = aElement.ownerDocument.defaultView; + + let x = rect.left, y = rect.top; + + // We need to compensate for any iframes that might shift things + // over. We also need to compensate for zooming. + let parentFrame = win.frameElement; + while (parentFrame) { + win = parentFrame.ownerDocument.defaultView; + let cstyle = win.getComputedStyle(parentFrame, ""); + + let framerect = parentFrame.getBoundingClientRect(); + x += framerect.left + parseFloat(cstyle.borderLeftWidth) + parseFloat(cstyle.paddingLeft); + y += framerect.top + parseFloat(cstyle.borderTopWidth) + parseFloat(cstyle.paddingTop); + + parentFrame = win.frameElement; + } + + if (aInScreenCoords) { + x += win.mozInnerScreenX; + y += win.mozInnerScreenY; + } + + let fullZoom = win.getInterface(Ci.nsIDOMWindowUtils).fullZoom; + rect = { + left: x * fullZoom, + top: y * fullZoom, + width: rect.width * fullZoom, + height: rect.height * fullZoom + }; + + return rect; + }, + + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + // Don't modify non-default targets or targets that aren't in top-level app + // tab docshells (isAppTab will be false for app tab subframes). + if (originalTarget != "" || !isAppTab) + return originalTarget; + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + let linkHost; + let docHost; + try { + linkHost = linkURI.host; + docHost = linkNode.ownerDocument.documentURIObject.host; + } catch (e) { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + // If we fail to get either host, just return originalTarget. + return originalTarget; + } + + if (docHost == linkHost) + return originalTarget; + + // Special case: ignore "www" prefix if it is part of host string + let [longHost, shortHost] = + linkHost.length > docHost.length ? [linkHost, docHost] : [docHost, linkHost]; + if (longHost == "www." + shortHost) + return originalTarget; + + return "_blank"; + }, + + /** + * Map the plugin's name to a filtered version more suitable for UI. + * + * @param aName The full-length name string of the plugin. + * @return the simplified name string. + */ + makeNicePluginName: function (aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + // Regex checks if aName begins with "Java" + non-letter char + if (/^Java\W/.exec(aName)) + return "Java"; + + // Clean up the plugin name by stripping off parenthetical clauses, + // trailing version numbers or "plugin". + // EG, "Foo Bar (Linux) Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/\(.*?\)/g, ""). + replace(/[\s\d\.\-\_\(\)]+$/, ""). + replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + /** + * Return true if linkNode has a rel="noreferrer" attribute. + * + * @param linkNode The element, or null. + * @return a boolean indicating if linkNode has a rel="noreferrer" attribute. + */ + linkHasNoReferrer: function (linkNode) { + // A null linkNode typically means that we're checking a link that wasn't + // provided via an link, like a text-selected URL. Don't leak + // referrer information in this case. + if (!linkNode) + return true; + + let rel = linkNode.getAttribute("rel"); + if (!rel) + return false; + + // The HTML spec says that rel should be split on spaces before looking + // for particular rel values. + let values = rel.split(/[ \t\r\n\f]/); + return values.indexOf('noreferrer') != -1; + }, + + /** + * Returns true if |mimeType| is text-based, or false otherwise. + * + * @param mimeType + * The MIME type to check. + */ + mimeTypeIsTextBased: function(mimeType) { + return mimeType.startsWith("text/") || + mimeType.endsWith("+xml") || + mimeType == "application/x-javascript" || + mimeType == "application/javascript" || + mimeType == "application/json" || + mimeType == "application/xml" || + mimeType == "mozilla.application/cached-xul"; + }, + + /** + * Return true if we should FAYT for this node + window (could be CPOW): + * + * @param elt + * The element that is focused + * @param win + * The window that is focused + * + */ + shouldFastFind: function(elt, win) { + if (elt) { + if (elt instanceof win.HTMLInputElement && elt.mozIsTextField(false)) + return false; + + if (elt.isContentEditable || win.document.designMode == "on") + return false; + + if (elt instanceof win.HTMLTextAreaElement || + elt instanceof win.HTMLSelectElement || + elt instanceof win.HTMLObjectElement || + elt instanceof win.HTMLEmbedElement) + return false; + } + + return true; + }, + + /** + * Return true if we can FAYT for this window (could be CPOW): + * + * @param win + * The top level window that is focused + * + */ + canFastFind: function(win) { + if (!win) + return false; + + if (!this.mimeTypeIsTextBased(win.document.contentType)) + return false; + + // disable FAYT in about:blank to prevent FAYT opening unexpectedly. + let loc = win.location; + if (loc.href == "about:blank") + return false; + + // disable FAYT in documents that ask for it to be disabled. + if ((loc.protocol == "about:" || loc.protocol == "chrome:") && + (win.document.documentElement && + win.document.documentElement.getAttribute("disablefastfind") == "true")) + return false; + + return true; + }, + + _visibleToolbarsMap: new WeakMap(), + + /** + * Return true if any or a specific toolbar that interacts with the content + * document is visible. + * + * @param {nsIDocShell} docShell The docShell instance that a toolbar should + * be interacting with + * @param {String} which Identifier of a specific toolbar + * @return {Boolean} + */ + isToolbarVisible(docShell, which) { + let window = this.getRootWindow(docShell); + if (!this._visibleToolbarsMap.has(window)) + return false; + let toolbars = this._visibleToolbarsMap.get(window); + return !!toolbars && toolbars.has(which); + }, + + /** + * Track whether a toolbar is visible for a given a docShell. + * + * @param {nsIDocShell} docShell The docShell instance that a toolbar should + * be interacting with + * @param {String} which Identifier of a specific toolbar + * @param {Boolean} [visible] Whether the toolbar is visible. Optional, + * defaults to `true`. + */ + trackToolbarVisibility(docShell, which, visible = true) { + // We have to get the root window object, because XPConnect WrappedNatives + // can't be used as WeakMap keys. + let window = this.getRootWindow(docShell); + let toolbars = this._visibleToolbarsMap.get(window); + if (!toolbars) { + toolbars = new Set(); + this._visibleToolbarsMap.set(window, toolbars); + } + if (!visible) + toolbars.delete(which); + else + toolbars.add(which); + }, + + /** + * Retrieve the root window object (i.e. the top-most content global) for a + * specific docShell object. + * + * @param {nsIDocShell} docShell + * @return {nsIDOMWindow} + */ + getRootWindow(docShell) { + return docShell.QueryInterface(Ci.nsIDocShellTreeItem) + .sameTypeRootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + }, + + getSelectionDetails: function(topWindow, aCharLen) { + // selections of more than 150 characters aren't useful + const kMaxSelectionLen = 150; + const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen); + + let focusedWindow = {}; + let focusedElement = Services.focus.getFocusedElementForWindow(topWindow, true, focusedWindow); + focusedWindow = focusedWindow.value; + + let selection = focusedWindow.getSelection(); + let selectionStr = selection.toString(); + + let collapsed = selection.isCollapsed; + + let url; + let linkText; + if (selectionStr) { + // Have some text, let's figure out if it looks like a URL that isn't + // actually a link. + linkText = selectionStr.trim(); + if (/^(?:https?|ftp):/i.test(linkText)) { + try { + url = this.makeURI(linkText); + } catch (ex) {} + } + // Check if this could be a valid url, just missing the protocol. + else if (/^(?:[a-z\d-]+\.)+[a-z]+$/i.test(linkText)) { + // Now let's see if this is an intentional link selection. Our guess is + // based on whether the selection begins/ends with whitespace or is + // preceded/followed by a non-word character. + + // selection.toString() trims trailing whitespace, so we look for + // that explicitly in the first and last ranges. + let beginRange = selection.getRangeAt(0); + let delimitedAtStart = /^\s/.test(beginRange); + if (!delimitedAtStart) { + let container = beginRange.startContainer; + let offset = beginRange.startOffset; + if (container.nodeType == container.TEXT_NODE && offset > 0) + delimitedAtStart = /\W/.test(container.textContent[offset - 1]); + else + delimitedAtStart = true; + } + + let delimitedAtEnd = false; + if (delimitedAtStart) { + let endRange = selection.getRangeAt(selection.rangeCount - 1); + delimitedAtEnd = /\s$/.test(endRange); + if (!delimitedAtEnd) { + let container = endRange.endContainer; + let offset = endRange.endOffset; + if (container.nodeType == container.TEXT_NODE && + offset < container.textContent.length) + delimitedAtEnd = /\W/.test(container.textContent[offset]); + else + delimitedAtEnd = true; + } + } + + if (delimitedAtStart && delimitedAtEnd) { + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"] + .getService(Ci.nsIURIFixup); + try { + url = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE); + } catch (ex) {} + } + } + } + + // try getting a selected text in text input. + if (!selectionStr && focusedElement instanceof Ci.nsIDOMNSEditableElement) { + // Don't get the selection for password fields. See bug 565717. + if (focusedElement instanceof Ci.nsIDOMHTMLTextAreaElement || + (focusedElement instanceof Ci.nsIDOMHTMLInputElement && + focusedElement.mozIsTextField(true))) { + selectionStr = focusedElement.editor.selection.toString(); + } + } + + if (selectionStr) { + if (selectionStr.length > charLen) { + // only use the first charLen important chars. see bug 221361 + var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}"); + pattern.test(selectionStr); + selectionStr = RegExp.lastMatch; + } + + selectionStr = selectionStr.trim().replace(/\s+/g, " "); + + if (selectionStr.length > charLen) { + selectionStr = selectionStr.substr(0, charLen); + } + } + + if (url && !url.host) { + url = null; + } + + return { text: selectionStr, docSelectionIsCollapsed: collapsed, + linkURL: url ? url.spec : null, linkText: url ? linkText : "" }; + }, + + // Iterates through every docshell in the window and calls PermitUnload. + canCloseWindow(window) { + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + let node = docShell.QueryInterface(Ci.nsIDocShellTreeItem); + for (let i = 0; i < node.childCount; ++i) { + let docShell = node.getChildAt(i).QueryInterface(Ci.nsIDocShell); + let contentViewer = docShell.contentViewer; + if (contentViewer && !contentViewer.permitUnload()) { + return false; + } + } + + return true; + }, + + /** + * Replaces %s or %S in the provided url or postData with the given parameter, + * acccording to the best charset for the given url. + * + * @return [url, postData] + * @throws if nor url nor postData accept a param, but a param was provided. + */ + parseUrlAndPostData: Task.async(function* (url, postData, param) { + let hasGETParam = /%s/i.test(url) + let decodedPostData = postData ? unescape(postData) : ""; + let hasPOSTParam = /%s/i.test(decodedPostData); + + if (!hasGETParam && !hasPOSTParam) { + if (param) { + // If nor the url, nor postData contain parameters, but a parameter was + // provided, return the original input. + throw new Error("A param was provided but there's nothing to bind it to"); + } + return [url, postData]; + } + + let charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + let matches = url.match(re); + if (matches) { + [, url, charset] = matches; + } else { + // Try to fetch a charset from History. + try { + // Will return an empty string if character-set is not found. + charset = yield PlacesUtils.getCharsetForURI(this.makeURI(url)); + } catch (ex) { + // makeURI() throws if url is invalid. + Cu.reportError(ex); + } + } + + // encodeURIComponent produces UTF-8, and cannot be used for other charsets. + // escape() works in those cases, but it doesn't uri-encode +, @, and /. + // Therefore we need to manually replace these ASCII characters by their + // encodeURIComponent result, to match the behavior of nsEscape() with + // url_XPAlphas. + let encodedParam = ""; + if (charset && charset != "UTF-8") { + try { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = charset; + encodedParam = converter.ConvertFromUnicode(param) + converter.Finish(); + } catch (ex) { + encodedParam = param; + } + encodedParam = escape(encodedParam).replace(/[+@\/]+/g, encodeURIComponent); + } else { + // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + } + + url = url.replace(/%s/g, encodedParam).replace(/%S/g, param); + if (hasPOSTParam) { + postData = decodedPostData.replace(/%s/g, encodedParam) + .replace(/%S/g, param); + } + return [url, postData]; + }), +}; diff --git a/toolkit/modules/CanonicalJSON.jsm b/toolkit/modules/CanonicalJSON.jsm new file mode 100644 index 000000000..ae754ff3a --- /dev/null +++ b/toolkit/modules/CanonicalJSON.jsm @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["CanonicalJSON"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "jsesc", + "resource://gre/modules/third_party/jsesc/jsesc.js"); + +this.CanonicalJSON = { + /** + * Return the canonical JSON form of the passed source, sorting all the object + * keys recursively. Note that this method will cause an infinite loop if + * cycles exist in the source (bug 1265357). + * + * @param source + * The elements to be serialized. + * + * The output will have all unicode chars escaped with the unicode codepoint + * as lowercase hexadecimal. + * + * @usage + * CanonicalJSON.stringify(listOfRecords); + **/ + stringify: function stringify(source) { + if (Array.isArray(source)) { + const jsonArray = source.map(x => typeof x === "undefined" ? null : x); + return `[${jsonArray.map(stringify).join(",")}]`; + } + + if (typeof source === "number") { + if (source === 0) { + return (Object.is(source, -0)) ? "-0" : "0"; + } + } + + // Leverage jsesc library, mainly for unicode escaping. + const toJSON = (input) => jsesc(input, {lowercaseHex: true, json: true}); + + if (typeof source !== "object" || source === null) { + return toJSON(source); + } + + // Dealing with objects, ordering keys. + const sortedKeys = Object.keys(source).sort(); + const lastIndex = sortedKeys.length - 1; + return sortedKeys.reduce((serial, key, index) => { + const value = source[key]; + // JSON.stringify drops keys with an undefined value. + if (typeof value === "undefined") { + return serial; + } + const jsonValue = value && value.toJSON ? value.toJSON() : value; + const suffix = index !== lastIndex ? "," : ""; + const escapedKey = toJSON(key); + return serial + `${escapedKey}:${stringify(jsonValue)}${suffix}`; + }, "{") + "}"; + }, +}; diff --git a/toolkit/modules/CertUtils.jsm b/toolkit/modules/CertUtils.jsm new file mode 100644 index 000000000..e61ea9de7 --- /dev/null +++ b/toolkit/modules/CertUtils.jsm @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +this.EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert", "readCertPrefs", "validateCert" ]; + +const Ce = Components.Exception; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Reads a set of expected certificate attributes from preferences. The returned + * array can be passed to validateCert or checkCert to validate that a + * certificate matches the expected attributes. The preferences should look like + * this: + * prefix.1.attribute1 + * prefix.1.attribute2 + * prefix.2.attribute1 + * etc. + * Each numeric branch contains a set of required attributes for a single + * certificate. Having multiple numeric branches means that multiple + * certificates would be accepted by validateCert. + * + * @param aPrefBranch + * The prefix for all preferences, should end with a ".". + * @return An array of JS objects with names / values corresponding to the + * expected certificate's attribute names / values. + */ +this.readCertPrefs = + function readCertPrefs(aPrefBranch) { + if (Services.prefs.getBranch(aPrefBranch).getChildList("").length == 0) + return null; + + let certs = []; + let counter = 1; + while (true) { + let prefBranchCert = Services.prefs.getBranch(aPrefBranch + counter + "."); + let prefCertAttrs = prefBranchCert.getChildList(""); + if (prefCertAttrs.length == 0) + break; + + let certAttrs = {}; + for (let prefCertAttr of prefCertAttrs) + certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr); + + certs.push(certAttrs); + counter++; + } + + return certs; +} + +/** + * Verifies that an nsIX509Cert matches the expected certificate attribute + * values. + * + * @param aCertificate + * The nsIX509Cert to compare to the expected attributes. + * @param aCerts + * An array of JS objects with names / values corresponding to the + * expected certificate's attribute names / values. If this is null or + * an empty array then no checks are performed. + * @throws NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the + * aCerts param does not exist or the value for a certificate attribute + * from the aCerts param is different than the expected value or + * aCertificate wasn't specified and aCerts is not null or an empty + * array. + */ +this.validateCert = + function validateCert(aCertificate, aCerts) { + // If there are no certificate requirements then just exit + if (!aCerts || aCerts.length == 0) + return; + + if (!aCertificate) { + const missingCertErr = "A required certificate was not present."; + Cu.reportError(missingCertErr); + throw new Ce(missingCertErr, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + var errors = []; + for (var i = 0; i < aCerts.length; ++i) { + var error = false; + var certAttrs = aCerts[i]; + for (var name in certAttrs) { + if (!(name in aCertificate)) { + error = true; + errors.push("Expected attribute '" + name + "' not present in " + + "certificate."); + break; + } + if (aCertificate[name] != certAttrs[name]) { + error = true; + errors.push("Expected certificate attribute '" + name + "' " + + "value incorrect, expected: '" + certAttrs[name] + + "', got: '" + aCertificate[name] + "'."); + break; + } + } + + if (!error) + break; + } + + if (error) { + errors.forEach(Cu.reportError.bind(Cu)); + const certCheckErr = "Certificate checks failed. See previous errors " + + "for details."; + Cu.reportError(certCheckErr); + throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE); + } +} + +/** + * Checks if the connection must be HTTPS and if so, only allows built-in + * certificates and validates application specified certificate attribute + * values. + * See bug 340198 and bug 544442. + * + * @param aChannel + * The nsIChannel that will have its certificate checked. + * @param aAllowNonBuiltInCerts (optional) + * When true certificates that aren't builtin are allowed. When false + * or not specified the certificate must be a builtin certificate. + * @param aCerts (optional) + * An array of JS objects with names / values corresponding to the + * channel's expected certificate's attribute names / values. If it + * isn't null or not specified the the scheme for the channel's + * originalURI must be https. + * @throws NS_ERROR_UNEXPECTED if a certificate is expected and the URI scheme + * is not https. + * NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the + * aCerts param does not exist or the value for a certificate attribute + * from the aCerts param is different than the expected value. + * NS_ERROR_ABORT if the certificate issuer is not built-in. + */ +this.checkCert = + function checkCert(aChannel, aAllowNonBuiltInCerts, aCerts) { + if (!aChannel.originalURI.schemeIs("https")) { + // Require https if there are certificate values to verify + if (aCerts) { + throw new Ce("SSL is required and URI scheme is not https.", + Cr.NS_ERROR_UNEXPECTED); + } + return; + } + + var cert = + aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider). + SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert; + + validateCert(cert, aCerts); + + if (aAllowNonBuiltInCerts === true) + return; + + var issuerCert = cert; + while (issuerCert.issuer && !issuerCert.issuer.equals(issuerCert)) + issuerCert = issuerCert.issuer; + + const certNotBuiltInErr = "Certificate issuer is not built-in."; + if (!issuerCert) + throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT); + + var tokenNames = issuerCert.getAllTokenNames({}); + + if (!tokenNames || !tokenNames.some(isBuiltinToken)) + throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT); +} + +function isBuiltinToken(tokenName) { + return tokenName == "Builtin Object Token"; +} + +/** + * This class implements nsIBadCertListener. Its job is to prevent "bad cert" + * security dialogs from being shown to the user. It is better to simply fail + * if the certificate is bad. See bug 304286. + * + * @param aAllowNonBuiltInCerts (optional) + * When true certificates that aren't builtin are allowed. When false + * or not specified the certificate must be a builtin certificate. + */ +this.BadCertHandler = + function BadCertHandler(aAllowNonBuiltInCerts) { + this.allowNonBuiltInCerts = aAllowNonBuiltInCerts; +} +BadCertHandler.prototype = { + + // nsIChannelEventSink + asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) { + if (this.allowNonBuiltInCerts) { + callback.onRedirectVerifyCallback(Components.results.NS_OK); + return; + } + + // make sure the certificate of the old channel checks out before we follow + // a redirect from it. See bug 340198. + // Don't call checkCert for internal redirects. See bug 569648. + if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL)) + checkCert(oldChannel); + + callback.onRedirectVerifyCallback(Components.results.NS_OK); + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Ci.nsIChannelEventSink) && + !iid.equals(Ci.nsIInterfaceRequestor) && + !iid.equals(Ci.nsISupports)) + throw Cr.NS_ERROR_NO_INTERFACE; + return this; + } +}; diff --git a/toolkit/modules/CharsetMenu.jsm b/toolkit/modules/CharsetMenu.jsm new file mode 100644 index 000000000..f6479c024 --- /dev/null +++ b/toolkit/modules/CharsetMenu.jsm @@ -0,0 +1,267 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = [ "CharsetMenu" ]; + +const { classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyGetter(this, "gBundle", function() { + const kUrl = "chrome://global/locale/charsetMenu.properties"; + return Services.strings.createBundle(kUrl); +}); + +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); + +const kAutoDetectors = [ + ["off", ""], + ["ja", "ja_parallel_state_machine"], + ["ru", "ruprob"], + ["uk", "ukprob"] +]; + +/** + * This set contains encodings that are in the Encoding Standard, except: + * - XSS-dangerous encodings (except ISO-2022-JP which is assumed to be + * too common not to be included). + * - x-user-defined, which practically never makes sense as an end-user-chosen + * override. + * - Encodings that IE11 doesn't have in its correspoding menu. + */ +const kEncodings = new Set([ + // Globally relevant + "UTF-8", + "windows-1252", + // Arabic + "windows-1256", + "ISO-8859-6", + // Baltic + "windows-1257", + "ISO-8859-4", + // "ISO-8859-13", // Hidden since not in menu in IE11 + // Central European + "windows-1250", + "ISO-8859-2", + // Chinese, Simplified + "gbk", + // Chinese, Traditional + "Big5", + // Cyrillic + "windows-1251", + "ISO-8859-5", + "KOI8-R", + "KOI8-U", + "IBM866", // Not in menu in Chromium. Maybe drop this? + // "x-mac-cyrillic", // Not in menu in IE11 or Chromium. + // Greek + "windows-1253", + "ISO-8859-7", + // Hebrew + "windows-1255", + "ISO-8859-8", + // Japanese + "Shift_JIS", + "EUC-JP", + "ISO-2022-JP", + // Korean + "EUC-KR", + // Thai + "windows-874", + // Turkish + "windows-1254", + // Vietnamese + "windows-1258", + // Hiding rare European encodings that aren't in the menu in IE11 and would + // make the menu messy by sorting all over the place + // "ISO-8859-3", + // "ISO-8859-10", + // "ISO-8859-14", + // "ISO-8859-15", + // "ISO-8859-16", + // "macintosh" +]); + +// Always at the start of the menu, in this order, followed by a separator. +const kPinned = [ + "UTF-8", + "windows-1252" +]; + +kPinned.forEach(x => kEncodings.delete(x)); + +function CharsetComparator(a, b) { + // Normal sorting sorts the part in parenthesis in an order that + // happens to make the less frequently-used items first. + let titleA = a.label.replace(/\(.*/, "") + b.value; + let titleB = b.label.replace(/\(.*/, "") + a.value; + // Secondarily reverse sort by encoding name to sort "windows" or + // "shift_jis" first. + return titleA.localeCompare(titleB) || b.value.localeCompare(a.value); +} + +function SetDetector(event) { + let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + str.data = event.target.getAttribute("detector"); + Services.prefs.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); +} + +function UpdateDetectorMenu(event) { + event.stopPropagation(); + let detector = Services.prefs.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString); + let menuitem = this.getElementsByAttribute("detector", detector).item(0); + if (menuitem) { + menuitem.setAttribute("checked", "true"); + } +} + +var gDetectorInfoCache, gCharsetInfoCache, gPinnedInfoCache; + +var CharsetMenu = { + build: function(parent, deprecatedShowAccessKeys=true, showDetector=true) { + if (!deprecatedShowAccessKeys) { + Deprecated.warning("CharsetMenu no longer supports building a menu with no access keys.", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1088710"); + } + function createDOMNode(doc, nodeInfo) { + let node = doc.createElement("menuitem"); + node.setAttribute("type", "radio"); + node.setAttribute("name", nodeInfo.name + "Group"); + node.setAttribute(nodeInfo.name, nodeInfo.value); + node.setAttribute("label", nodeInfo.label); + if (nodeInfo.accesskey) { + node.setAttribute("accesskey", nodeInfo.accesskey); + } + return node; + } + + if (parent.hasChildNodes()) { + // Detector menu or charset menu already built + return; + } + this._ensureDataReady(); + let doc = parent.ownerDocument; + + if (showDetector) { + let menuNode = doc.createElement("menu"); + menuNode.setAttribute("label", gBundle.GetStringFromName("charsetMenuAutodet")); + menuNode.setAttribute("accesskey", gBundle.GetStringFromName("charsetMenuAutodet.key")); + parent.appendChild(menuNode); + + let menuPopupNode = doc.createElement("menupopup"); + menuNode.appendChild(menuPopupNode); + menuPopupNode.addEventListener("command", SetDetector); + menuPopupNode.addEventListener("popupshown", UpdateDetectorMenu); + + gDetectorInfoCache.forEach(detectorInfo => menuPopupNode.appendChild(createDOMNode(doc, detectorInfo))); + parent.appendChild(doc.createElement("menuseparator")); + } + + gPinnedInfoCache.forEach(charsetInfo => parent.appendChild(createDOMNode(doc, charsetInfo))); + parent.appendChild(doc.createElement("menuseparator")); + gCharsetInfoCache.forEach(charsetInfo => parent.appendChild(createDOMNode(doc, charsetInfo))); + }, + + getData: function() { + this._ensureDataReady(); + return { + detectors: gDetectorInfoCache, + pinnedCharsets: gPinnedInfoCache, + otherCharsets: gCharsetInfoCache + }; + }, + + _ensureDataReady: function() { + if (!gDetectorInfoCache) { + gDetectorInfoCache = this.getDetectorInfo(); + gPinnedInfoCache = this.getCharsetInfo(kPinned, false); + gCharsetInfoCache = this.getCharsetInfo(kEncodings); + } + }, + + getDetectorInfo: function() { + return kAutoDetectors.map(([detectorName, nodeId]) => ({ + label: this._getDetectorLabel(detectorName), + accesskey: this._getDetectorAccesskey(detectorName), + name: "detector", + value: nodeId + })); + }, + + getCharsetInfo: function(charsets, sort=true) { + let list = Array.from(charsets, charset => ({ + label: this._getCharsetLabel(charset), + accesskey: this._getCharsetAccessKey(charset), + name: "charset", + value: charset + })); + + if (sort) { + list.sort(CharsetComparator); + } + return list; + }, + + _getDetectorLabel: function(detector) { + try { + return gBundle.GetStringFromName("charsetMenuAutodet." + detector); + } catch (ex) {} + return detector; + }, + _getDetectorAccesskey: function(detector) { + try { + return gBundle.GetStringFromName("charsetMenuAutodet." + detector + ".key"); + } catch (ex) {} + return ""; + }, + + _getCharsetLabel: function(charset) { + if (charset == "gbk") { + // Localization key has been revised + charset = "gbk.bis"; + } + try { + return gBundle.GetStringFromName(charset); + } catch (ex) {} + return charset; + }, + _getCharsetAccessKey: function(charset) { + if (charset == "gbk") { + // Localization key has been revised + charset = "gbk.bis"; + } + try { + return gBundle.GetStringFromName(charset + ".key"); + } catch (ex) {} + return ""; + }, + + /** + * For substantially similar encodings, treat two encodings as the same + * for the purpose of the check mark. + */ + foldCharset: function(charset) { + switch (charset) { + case "ISO-8859-8-I": + return "windows-1255"; + + case "gb18030": + return "gbk"; + + default: + return charset; + } + }, + + update: function(parent, charset) { + let menuitem = parent.getElementsByAttribute("charset", this.foldCharset(charset)).item(0); + if (menuitem) { + menuitem.setAttribute("checked", "true"); + } + }, +}; + +Object.freeze(CharsetMenu); + diff --git a/toolkit/modules/ClientID.jsm b/toolkit/modules/ClientID.jsm new file mode 100644 index 000000000..e29e1ee30 --- /dev/null +++ b/toolkit/modules/ClientID.jsm @@ -0,0 +1,231 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["ClientID"]; + +const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; + +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "ClientID::"; + +XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", + "resource://services-common/utils.js"); + +XPCOMUtils.defineLazyGetter(this, "gDatareportingPath", () => { + return OS.Path.join(OS.Constants.Path.profileDir, "datareporting"); +}); + +XPCOMUtils.defineLazyGetter(this, "gStateFilePath", () => { + return OS.Path.join(gDatareportingPath, "state.json"); +}); + +const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID"; + +/** + * Checks if client ID has a valid format. + * + * @param {String} id A string containing the client ID. + * @return {Boolean} True when the client ID has valid format, or False + * otherwise. + */ +function isValidClientID(id) { + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + return UUID_REGEX.test(id); +} + +this.ClientID = Object.freeze({ + /** + * This returns a promise resolving to the the stable client ID we use for + * data reporting (FHR & Telemetry). Previously exising FHR client IDs are + * migrated to this. + * + * WARNING: This functionality is duplicated for Android (see GeckoProfile.getClientId + * for more). There are Java tests (TestGeckoProfile) to ensure the functionality is + * consistent and Gecko tests to come (bug 1249156). However, THIS IS NOT FOOLPROOF. + * Be careful when changing this code and, in particular, the underlying file format. + * + * @return {Promise} The stable client ID. + */ + getClientID: function() { + return ClientIDImpl.getClientID(); + }, + +/** + * Get the client id synchronously without hitting the disk. + * This returns: + * - the current on-disk client id if it was already loaded + * - the client id that we cached into preferences (if any) + * - null otherwise + */ + getCachedClientID: function() { + return ClientIDImpl.getCachedClientID(); + }, + + /** + * Only used for testing. Invalidates the client ID so that it gets read + * again from file. + */ + _reset: function() { + return ClientIDImpl._reset(); + }, +}); + +var ClientIDImpl = { + _clientID: null, + _loadClientIdTask: null, + _saveClientIdTask: null, + _logger: null, + + _loadClientID: function () { + if (this._loadClientIdTask) { + return this._loadClientIdTask; + } + + this._loadClientIdTask = this._doLoadClientID(); + let clear = () => this._loadClientIdTask = null; + this._loadClientIdTask.then(clear, clear); + return this._loadClientIdTask; + }, + + _doLoadClientID: Task.async(function* () { + // As we want to correlate FHR and telemetry data (and move towards unifying the two), + // we first moved the ID management from the FHR implementation to the datareporting + // service, then to a common shared module. + // Consequently, we try to import an existing FHR ID, so we can keep using it. + + // Try to load the client id from the DRS state file first. + try { + let state = yield CommonUtils.readJSON(gStateFilePath); + if (state && this.updateClientID(state.clientID)) { + return this._clientID; + } + } catch (e) { + // fall through to next option + } + + // If we dont have DRS state yet, try to import from the FHR state. + try { + let fhrStatePath = OS.Path.join(OS.Constants.Path.profileDir, "healthreport", "state.json"); + let state = yield CommonUtils.readJSON(fhrStatePath); + if (state && this.updateClientID(state.clientID)) { + this._saveClientID(); + return this._clientID; + } + } catch (e) { + // fall through to next option + } + + // We dont have an id from FHR yet, generate a new ID. + this.updateClientID(CommonUtils.generateUUID()); + this._saveClientIdTask = this._saveClientID(); + + // Wait on persisting the id. Otherwise failure to save the ID would result in + // the client creating and subsequently sending multiple IDs to the server. + // This would appear as multiple clients submitting similar data, which would + // result in orphaning. + yield this._saveClientIdTask; + + return this._clientID; + }), + + /** + * Save the client ID to the client ID file. + * + * @return {Promise} A promise resolved when the client ID is saved to disk. + */ + _saveClientID: Task.async(function* () { + let obj = { clientID: this._clientID }; + yield OS.File.makeDir(gDatareportingPath); + yield CommonUtils.writeJSON(obj, gStateFilePath); + this._saveClientIdTask = null; + }), + + /** + * This returns a promise resolving to the the stable client ID we use for + * data reporting (FHR & Telemetry). Previously exising FHR client IDs are + * migrated to this. + * + * @return {Promise} The stable client ID. + */ + getClientID: function() { + if (!this._clientID) { + return this._loadClientID(); + } + + return Promise.resolve(this._clientID); + }, + + /** + * Get the client id synchronously without hitting the disk. + * This returns: + * - the current on-disk client id if it was already loaded + * - the client id that we cached into preferences (if any) + * - null otherwise + */ + getCachedClientID: function() { + if (this._clientID) { + // Already loaded the client id from disk. + return this._clientID; + } + + // Not yet loaded, return the cached client id if we have one. + let id = Preferences.get(PREF_CACHED_CLIENTID, null); + if (id === null) { + return null; + } + if (!isValidClientID(id)) { + this._log.error("getCachedClientID - invalid client id in preferences, resetting", id); + Preferences.reset(PREF_CACHED_CLIENTID); + return null; + } + return id; + }, + + /* + * Resets the provider. This is for testing only. + */ + _reset: Task.async(function* () { + yield this._loadClientIdTask; + yield this._saveClientIdTask; + this._clientID = null; + }), + + /** + * Sets the client id to the given value and updates the value cached in + * preferences only if the given id is a valid. + * + * @param {String} id A string containing the client ID. + * @return {Boolean} True when the client ID has valid format, or False + * otherwise. + */ + updateClientID: function (id) { + if (!isValidClientID(id)) { + this._log.error("updateClientID - invalid client ID", id); + return false; + } + + this._clientID = id; + Preferences.set(PREF_CACHED_CLIENTID, this._clientID); + return true; + }, + + /** + * A helper for getting access to telemetry logger. + */ + get _log() { + if (!this._logger) { + this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + } + + return this._logger; + }, +}; diff --git a/toolkit/modules/Color.jsm b/toolkit/modules/Color.jsm new file mode 100644 index 000000000..00a9bd953 --- /dev/null +++ b/toolkit/modules/Color.jsm @@ -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/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Color"]; + +/** + * Color class, which describes a color. + * In the future, this object may be extended to allow for conversions between + * different color formats and notations, support transparency. + * + * @param {Number} r Red color component + * @param {Number} g Green color component + * @param {Number} b Blue color component + */ +function Color(r, g, b) { + this.r = r; + this.g = g; + this.b = b; +} + +Color.prototype = { + /** + * Formula from W3C's WCAG 2.0 spec's relative luminance, section 1.4.1, + * http://www.w3.org/TR/WCAG20/. + * + * @return {Number} Relative luminance, represented as number between 0 and 1. + */ + get relativeLuminance() { + let colorArr = [this.r, this.b, this.g].map(color => { + color = parseInt(color, 10); + if (color <= 10) + return color / 255 / 12.92; + return Math.pow(((color / 255) + 0.055) / 1.055, 2.4); + }); + return colorArr[0] * 0.2126 + + colorArr[1] * 0.7152 + + colorArr[2] * 0.0722; + }, + + /** + * @return {Boolean} TRUE if the color value can be considered bright. + */ + get isBright() { + // Note: this is a high enough value to be considered as 'bright', but was + // decided upon empirically. + return this.relativeLuminance > 0.7; + }, + + /** + * Get the contrast ratio between the current color and a second other color. + * A common use case is to express the difference between a foreground and a + * background color in numbers. + * Formula from W3C's WCAG 2.0 spec's contrast ratio, section 1.4.1, + * http://www.w3.org/TR/WCAG20/. + * + * @param {Color} otherColor Color instance to calculate the contrast with + * @return {Number} Contrast ratios can range from 1 to 21, commonly written + * as 1:1 to 21:1. + */ + contrastRatio(otherColor) { + if (!(otherColor instanceof Color)) + throw new TypeError("The first argument should be an instance of Color"); + + let luminance = this.relativeLuminance; + let otherLuminance = otherColor.relativeLuminance; + return (Math.max(luminance, otherLuminance) + 0.05) / + (Math.min(luminance, otherLuminance) + 0.05); + }, + + /** + * Biased method to check if the contrast ratio between two colors is high + * enough to be discernable. + * + * @param {Color} otherColor Color instance to calculate the contrast with + * @return {Boolean} + */ + isContrastRatioAcceptable(otherColor) { + // Note: this is a high enough value to be considered as 'high contrast', + // but was decided upon empirically. + return this.contrastRatio(otherColor) > 3; + } +}; diff --git a/toolkit/modules/Console.jsm b/toolkit/modules/Console.jsm new file mode 100644 index 000000000..8cf63bcf0 --- /dev/null +++ b/toolkit/modules/Console.jsm @@ -0,0 +1,713 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +/** + * Define a 'console' API to roughly match the implementation provided by + * Firebug. + * This module helps cases where code is shared between the web and Firefox. + * See also Browser.jsm for an implementation of other web constants to help + * sharing code between the web and firefox; + * + * The API is only be a rough approximation for 3 reasons: + * - The Firebug console API is implemented in many places with differences in + * the implementations, so there isn't a single reference to adhere to + * - The Firebug console is a rich display compared with dump(), so there will + * be many things that we can't replicate + * - The primary use of this API is debugging and error logging so the perfect + * implementation isn't always required (or even well defined) + */ + +this.EXPORTED_SYMBOLS = [ "console", "ConsoleAPI" ]; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); + +var gTimerRegistry = new Map(); + +/** + * String utility to ensure that strings are a specified length. Strings + * that are too long are truncated to the max length and the last char is + * set to "_". Strings that are too short are padded with spaces. + * + * @param {string} aStr + * The string to format to the correct length + * @param {number} aMaxLen + * The maximum allowed length of the returned string + * @param {number} aMinLen (optional) + * The minimum allowed length of the returned string. If undefined, + * then aMaxLen will be used + * @param {object} aOptions (optional) + * An object allowing format customization. Allowed customizations: + * 'truncate' - can take the value "start" to truncate strings from + * the start as opposed to the end or "center" to truncate + * strings in the center. + * 'align' - takes an alignment when padding is needed for MinLen, + * either "start" or "end". Defaults to "start". + * @return {string} + * The original string formatted to fit the specified lengths + */ +function fmt(aStr, aMaxLen, aMinLen, aOptions) { + if (aMinLen == null) { + aMinLen = aMaxLen; + } + if (aStr == null) { + aStr = ""; + } + if (aStr.length > aMaxLen) { + if (aOptions && aOptions.truncate == "start") { + return "_" + aStr.substring(aStr.length - aMaxLen + 1); + } + else if (aOptions && aOptions.truncate == "center") { + let start = aStr.substring(0, (aMaxLen / 2)); + + let end = aStr.substring((aStr.length - (aMaxLen / 2)) + 1); + return start + "_" + end; + } + return aStr.substring(0, aMaxLen - 1) + "_"; + } + if (aStr.length < aMinLen) { + let padding = Array(aMinLen - aStr.length + 1).join(" "); + aStr = (aOptions.align === "end") ? padding + aStr : aStr + padding; + } + return aStr; +} + +/** + * Utility to extract the constructor name of an object. + * Object.toString gives: "[object ?????]"; we want the "?????". + * + * @param {object} aObj + * The object from which to extract the constructor name + * @return {string} + * The constructor name + */ +function getCtorName(aObj) { + if (aObj === null) { + return "null"; + } + if (aObj === undefined) { + return "undefined"; + } + if (aObj.constructor && aObj.constructor.name) { + return aObj.constructor.name; + } + // If that fails, use Objects toString which sometimes gives something + // better than 'Object', and at least defaults to Object if nothing better + return Object.prototype.toString.call(aObj).slice(8, -1); +} + +/** + * Indicates whether an object is a JS or `Components.Exception` error. + * + * @param {object} aThing + The object to check + * @return {boolean} + Is this object an error? + */ +function isError(aThing) { + return aThing && ( + (typeof aThing.name == "string" && + aThing.name.startsWith("NS_ERROR_")) || + getCtorName(aThing).endsWith("Error")); +} + +/** + * A single line stringification of an object designed for use by humans + * + * @param {any} aThing + * The object to be stringified + * @param {boolean} aAllowNewLines + * @return {string} + * A single line representation of aThing, which will generally be at + * most 80 chars long + */ +function stringify(aThing, aAllowNewLines) { + if (aThing === undefined) { + return "undefined"; + } + + if (aThing === null) { + return "null"; + } + + if (isError(aThing)) { + return "Message: " + aThing; + } + + if (typeof aThing == "object") { + let type = getCtorName(aThing); + if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) { + return debugElement(aThing); + } + type = (type == "Object" ? "" : type + " "); + let json; + try { + json = JSON.stringify(aThing); + } + catch (ex) { + // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled + json = "{" + Object.keys(aThing).join(":..,") + ":.., " + "}"; + } + return type + json; + } + + if (typeof aThing == "function") { + return aThing.toString().replace(/\s+/g, " "); + } + + let str = aThing.toString(); + if (!aAllowNewLines) { + str = str.replace(/\n/g, "|"); + } + return str; +} + +/** + * Create a simple debug representation of a given element. + * + * @param {nsIDOMElement} aElement + * The element to debug + * @return {string} + * A simple single line representation of aElement + */ +function debugElement(aElement) { + return "<" + aElement.tagName + + (aElement.id ? "#" + aElement.id : "") + + (aElement.className && aElement.className.split ? + "." + aElement.className.split(" ").join(" .") : + "") + + ">"; +} + +/** + * A multi line stringification of an object, designed for use by humans + * + * @param {any} aThing + * The object to be stringified + * @return {string} + * A multi line representation of aThing + */ +function log(aThing) { + if (aThing === null) { + return "null\n"; + } + + if (aThing === undefined) { + return "undefined\n"; + } + + if (typeof aThing == "object") { + let reply = ""; + let type = getCtorName(aThing); + if (type == "Map") { + reply += "Map\n"; + for (let [key, value] of aThing) { + reply += logProperty(key, value); + } + } + else if (type == "Set") { + let i = 0; + reply += "Set\n"; + for (let value of aThing) { + reply += logProperty('' + i, value); + i++; + } + } + else if (isError(aThing)) { + reply += " Message: " + aThing + "\n"; + if (aThing.stack) { + reply += " Stack:\n"; + var frame = aThing.stack; + while (frame) { + reply += " " + frame + "\n"; + frame = frame.caller; + } + } + } + else if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) { + reply += " " + debugElement(aThing) + "\n"; + } + else { + let keys = Object.getOwnPropertyNames(aThing); + if (keys.length > 0) { + reply += type + "\n"; + keys.forEach(function(aProp) { + reply += logProperty(aProp, aThing[aProp]); + }); + } + else { + reply += type + "\n"; + let root = aThing; + let logged = []; + while (root != null) { + let properties = Object.keys(root); + properties.sort(); + properties.forEach(function(property) { + if (!(property in logged)) { + logged[property] = property; + reply += logProperty(property, aThing[property]); + } + }); + + root = Object.getPrototypeOf(root); + if (root != null) { + reply += ' - prototype ' + getCtorName(root) + '\n'; + } + } + } + } + + return reply; + } + + return " " + aThing.toString() + "\n"; +} + +/** + * Helper for log() which converts a property/value pair into an output + * string + * + * @param {string} aProp + * The name of the property to include in the output string + * @param {object} aValue + * Value assigned to aProp to be converted to a single line string + * @return {string} + * Multi line output string describing the property/value pair + */ +function logProperty(aProp, aValue) { + let reply = ""; + if (aProp == "stack" && typeof value == "string") { + let trace = parseStack(aValue); + reply += formatTrace(trace); + } + else { + reply += " - " + aProp + " = " + stringify(aValue) + "\n"; + } + return reply; +} + +const LOG_LEVELS = { + "all": Number.MIN_VALUE, + "debug": 2, + "log": 3, + "info": 3, + "clear": 3, + "trace": 3, + "timeEnd": 3, + "time": 3, + "group": 3, + "groupEnd": 3, + "dir": 3, + "dirxml": 3, + "warn": 4, + "error": 5, + "off": Number.MAX_VALUE, +}; + +/** + * Helper to tell if a console message of `aLevel` type + * should be logged in stdout and sent to consoles given + * the current maximum log level being defined in `console.maxLogLevel` + * + * @param {string} aLevel + * Console message log level + * @param {string} aMaxLevel {string} + * String identifier (See LOG_LEVELS for possible + * values) that allows to filter which messages + * are logged based on their log level + * @return {boolean} + * Should this message be logged or not? + */ +function shouldLog(aLevel, aMaxLevel) { + return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel]; +} + +/** + * Parse a stack trace, returning an array of stack frame objects, where + * each has filename/lineNumber/functionName members + * + * @param {string} aStack + * The serialized stack trace + * @return {object[]} + * Array of { file: "...", line: NNN, call: "..." } objects + */ +function parseStack(aStack) { + let trace = []; + aStack.split("\n").forEach(function(line) { + if (!line) { + return; + } + let at = line.lastIndexOf("@"); + let posn = line.substring(at + 1); + trace.push({ + filename: posn.split(":")[0], + lineNumber: posn.split(":")[1], + functionName: line.substring(0, at) + }); + }); + return trace; +} + +/** + * Format a frame coming from Components.stack such that it can be used by the + * Browser Console, via console-api-log-event notifications. + * + * @param {object} aFrame + * The stack frame from which to begin the walk. + * @param {number=0} aMaxDepth + * Maximum stack trace depth. Default is 0 - no depth limit. + * @return {object[]} + * An array of {filename, lineNumber, functionName, language} objects. + * These objects follow the same format as other console-api-log-event + * messages. + */ +function getStack(aFrame, aMaxDepth = 0) { + if (!aFrame) { + aFrame = Components.stack.caller; + } + let trace = []; + while (aFrame) { + trace.push({ + filename: aFrame.filename, + lineNumber: aFrame.lineNumber, + functionName: aFrame.name, + language: aFrame.language, + }); + if (aMaxDepth == trace.length) { + break; + } + aFrame = aFrame.caller; + } + return trace; +} + +/** + * Take the output from parseStack() and convert it to nice readable + * output + * + * @param {object[]} aTrace + * Array of trace objects as created by parseStack() + * @return {string} Multi line report of the stack trace + */ +function formatTrace(aTrace) { + let reply = ""; + aTrace.forEach(function(frame) { + reply += fmt(frame.filename, 20, 20, { truncate: "start" }) + " " + + fmt(frame.lineNumber, 5, 5) + " " + + fmt(frame.functionName, 75, 0, { truncate: "center" }) + "\n"; + }); + return reply; +} + +/** + * Create a new timer by recording the current time under the specified name. + * + * @param {string} aName + * The name of the timer. + * @param {number} [aTimestamp=Date.now()] + * Optional timestamp that tells when the timer was originally started. + * @return {object} + * The name property holds the timer name and the started property + * holds the time the timer was started. In case of error, it returns + * an object with the single property "error" that contains the key + * for retrieving the localized error message. + */ +function startTimer(aName, aTimestamp) { + let key = aName.toString(); + if (!gTimerRegistry.has(key)) { + gTimerRegistry.set(key, aTimestamp || Date.now()); + } + return { name: aName, started: gTimerRegistry.get(key) }; +} + +/** + * Stop the timer with the specified name and retrieve the elapsed time. + * + * @param {string} aName + * The name of the timer. + * @param {number} [aTimestamp=Date.now()] + * Optional timestamp that tells when the timer was originally stopped. + * @return {object} + * The name property holds the timer name and the duration property + * holds the number of milliseconds since the timer was started. + */ +function stopTimer(aName, aTimestamp) { + let key = aName.toString(); + let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key); + gTimerRegistry.delete(key); + return { name: aName, duration: duration }; +} + +/** + * Dump a new message header to stdout by taking care of adding an eventual + * prefix + * + * @param {object} aConsole + * ConsoleAPI instance + * @param {string} aLevel + * The string identifier for the message log level + * @param {string} aMessage + * The string message to print to stdout + */ +function dumpMessage(aConsole, aLevel, aMessage) { + aConsole.dump( + "console." + aLevel + ": " + + (aConsole.prefix ? aConsole.prefix + ": " : "") + + aMessage + "\n" + ); +} + +/** + * Create a function which will output a concise level of output when used + * as a logging function + * + * @param {string} aLevel + * A prefix to all output generated from this function detailing the + * level at which output occurred + * @return {function} + * A logging function + * @see createMultiLineDumper() + */ +function createDumper(aLevel) { + return function() { + if (!shouldLog(aLevel, this.maxLogLevel)) { + return; + } + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + sendConsoleAPIMessage(this, aLevel, frame, args); + let data = args.map(function(arg) { + return stringify(arg, true); + }); + dumpMessage(this, aLevel, data.join(" ")); + }; +} + +/** + * Create a function which will output more detailed level of output when + * used as a logging function + * + * @param {string} aLevel + * A prefix to all output generated from this function detailing the + * level at which output occurred + * @return {function} + * A logging function + * @see createDumper() + */ +function createMultiLineDumper(aLevel) { + return function() { + if (!shouldLog(aLevel, this.maxLogLevel)) { + return; + } + dumpMessage(this, aLevel, ""); + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + sendConsoleAPIMessage(this, aLevel, frame, args); + args.forEach(function(arg) { + this.dump(log(arg)); + }, this); + }; +} + +/** + * Send a Console API message. This function will send a console-api-log-event + * notification through the nsIObserverService. + * + * @param {object} aConsole + * The instance of ConsoleAPI performing the logging. + * @param {string} aLevel + * Message severity level. This is usually the name of the console method + * that was called. + * @param {object} aFrame + * The youngest stack frame coming from Components.stack, as formatted by + * getStack(). + * @param {array} aArgs + * The arguments given to the console method. + * @param {object} aOptions + * Object properties depend on the console method that was invoked: + * - timer: for time() and timeEnd(). Holds the timer information. + * - groupName: for group(), groupCollapsed() and groupEnd(). + * - stacktrace: for trace(). Holds the array of stack frames as given by + * getStack(). + */ +function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) +{ + let consoleEvent = { + ID: "jsm", + innerID: aConsole.innerID || aFrame.filename, + consoleID: aConsole.consoleID, + level: aLevel, + filename: aFrame.filename, + lineNumber: aFrame.lineNumber, + functionName: aFrame.functionName, + timeStamp: Date.now(), + arguments: aArgs, + prefix: aConsole.prefix, + }; + + consoleEvent.wrappedJSObject = consoleEvent; + + switch (aLevel) { + case "trace": + consoleEvent.stacktrace = aOptions.stacktrace; + break; + case "time": + case "timeEnd": + consoleEvent.timer = aOptions.timer; + break; + case "group": + case "groupCollapsed": + case "groupEnd": + try { + consoleEvent.groupName = Array.prototype.join.call(aArgs, " "); + } + catch (ex) { + Cu.reportError(ex); + Cu.reportError(ex.stack); + return; + } + break; + } + + let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] + .getService(Ci.nsIConsoleAPIStorage); + if (ConsoleAPIStorage) { + ConsoleAPIStorage.recordEvent("jsm", null, consoleEvent); + } +} + +/** + * This creates a console object that somewhat replicates Firebug's console + * object + * + * @param {object} aConsoleOptions + * Optional dictionary with a set of runtime console options: + * - prefix {string} : An optional prefix string to be printed before + * the actual logged message + * - maxLogLevel {string} : String identifier (See LOG_LEVELS for + * possible values) that allows to filter which + * messages are logged based on their log level. + * If falsy value, all messages will be logged. + * If wrong value that doesn't match any key of + * LOG_LEVELS, no message will be logged + * - maxLogLevelPref {string} : String pref name which contains the + * level to use for maxLogLevel. If the pref doesn't + * exist or gets removed, the maxLogLevel will default + * to the value passed to this constructor (or "all" + * if it wasn't specified). + * - dump {function} : An optional function to intercept all strings + * written to stdout + * - innerID {string}: An ID representing the source of the message. + * Normally the inner ID of a DOM window. + * - consoleID {string} : String identified for the console, this will + * be passed through the console notifications + * @return {object} + * A console API instance object + */ +function ConsoleAPI(aConsoleOptions = {}) { + // Normalize console options to set default values + // in order to avoid runtime checks on each console method call. + this.dump = aConsoleOptions.dump || dump; + this.prefix = aConsoleOptions.prefix || ""; + this.maxLogLevel = aConsoleOptions.maxLogLevel; + this.innerID = aConsoleOptions.innerID || null; + this.consoleID = aConsoleOptions.consoleID || ""; + + // Setup maxLogLevelPref watching + let updateMaxLogLevel = () => { + if (Services.prefs.getPrefType(aConsoleOptions.maxLogLevelPref) == Services.prefs.PREF_STRING) { + this._maxLogLevel = Services.prefs.getCharPref(aConsoleOptions.maxLogLevelPref).toLowerCase(); + } else { + this._maxLogLevel = this._maxExplicitLogLevel; + } + }; + + if (aConsoleOptions.maxLogLevelPref) { + updateMaxLogLevel(); + Services.prefs.addObserver(aConsoleOptions.maxLogLevelPref, updateMaxLogLevel, false); + } + + // Bind all the functions to this object. + for (let prop in this) { + if (typeof(this[prop]) === "function") { + this[prop] = this[prop].bind(this); + } + } +} + +ConsoleAPI.prototype = { + /** + * The last log level that was specified via the constructor or setter. This + * is used as a fallback if the pref doesn't exist or is removed. + */ + _maxExplicitLogLevel: null, + /** + * The current log level via all methods of setting (pref or via the API). + */ + _maxLogLevel: null, + debug: createMultiLineDumper("debug"), + log: createDumper("log"), + info: createDumper("info"), + warn: createDumper("warn"), + error: createMultiLineDumper("error"), + exception: createMultiLineDumper("error"), + + trace: function Console_trace() { + if (!shouldLog("trace", this.maxLogLevel)) { + return; + } + let args = Array.prototype.slice.call(arguments, 0); + let trace = getStack(Components.stack.caller); + sendConsoleAPIMessage(this, "trace", trace[0], args, + { stacktrace: trace }); + dumpMessage(this, "trace", "\n" + formatTrace(trace)); + }, + clear: function Console_clear() {}, + + dir: createMultiLineDumper("dir"), + dirxml: createMultiLineDumper("dirxml"), + group: createDumper("group"), + groupEnd: createDumper("groupEnd"), + + time: function Console_time() { + if (!shouldLog("time", this.maxLogLevel)) { + return; + } + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + let timer = startTimer(args[0]); + sendConsoleAPIMessage(this, "time", frame, args, { timer: timer }); + dumpMessage(this, "time", + "'" + timer.name + "' @ " + (new Date())); + }, + + timeEnd: function Console_timeEnd() { + if (!shouldLog("timeEnd", this.maxLogLevel)) { + return; + } + let args = Array.prototype.slice.call(arguments, 0); + let frame = getStack(Components.stack.caller, 1)[0]; + let timer = stopTimer(args[0]); + sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer: timer }); + dumpMessage(this, "timeEnd", + "'" + timer.name + "' " + timer.duration + "ms"); + }, + + get maxLogLevel() { + return this._maxLogLevel || "all"; + }, + + set maxLogLevel(aValue) { + this._maxLogLevel = this._maxExplicitLogLevel = aValue; + }, +}; + +this.console = new ConsoleAPI(); +this.ConsoleAPI = ConsoleAPI; diff --git a/toolkit/modules/DateTimePickerHelper.jsm b/toolkit/modules/DateTimePickerHelper.jsm new file mode 100644 index 000000000..398687988 --- /dev/null +++ b/toolkit/modules/DateTimePickerHelper.jsm @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const DEBUG = false; +function debug(aStr) { + if (DEBUG) { + dump("-*- DateTimePickerHelper: " + aStr + "\n"); + } +} + +this.EXPORTED_SYMBOLS = [ + "DateTimePickerHelper" +]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +/* + * DateTimePickerHelper receives message from content side (input box) and + * is reposible for opening, closing and updating the picker. Similary, + * DateTimePickerHelper listens for picker's events and notifies the content + * side (input box) about them. + */ +this.DateTimePickerHelper = { + picker: null, + weakBrowser: null, + + MESSAGES: [ + "FormDateTime:OpenPicker", + "FormDateTime:ClosePicker", + "FormDateTime:UpdatePicker" + ], + + init: function() { + for (let msg of this.MESSAGES) { + Services.mm.addMessageListener(msg, this); + } + }, + + uninit: function() { + for (let msg of this.MESSAGES) { + Services.mm.removeMessageListener(msg, this); + } + }, + + // nsIMessageListener + receiveMessage: function(aMessage) { + debug("receiveMessage: " + aMessage.name); + switch (aMessage.name) { + case "FormDateTime:OpenPicker": { + this.showPicker(aMessage.target, aMessage.data); + break; + } + case "FormDateTime:ClosePicker": { + if (!this.picker) { + return; + } + this.picker.closePicker(); + break; + } + case "FormDateTime:UpdatePicker": { + this.picker.setPopupValue(aMessage.data); + break; + } + default: + break; + } + }, + + // nsIDOMEventListener + handleEvent: function(aEvent) { + debug("handleEvent: " + aEvent.type); + switch (aEvent.type) { + case "DateTimePickerValueChanged": { + this.updateInputBoxValue(aEvent); + break; + } + case "popuphidden": { + let browser = this.weakBrowser ? this.weakBrowser.get() : null; + if (browser) { + browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed"); + } + this.close(); + break; + } + default: + break; + } + }, + + // Called when picker value has changed, notify input box about it. + updateInputBoxValue: function(aEvent) { + // TODO: parse data based on input type. + const { hour, minute } = aEvent.detail; + debug("hour: " + hour + ", minute: " + minute); + let browser = this.weakBrowser ? this.weakBrowser.get() : null; + if (browser) { + browser.messageManager.sendAsyncMessage( + "FormDateTime:PickerValueChanged", { hour, minute }); + } + }, + + // Get picker from browser and show it anchored to the input box. + showPicker: function(aBrowser, aData) { + let rect = aData.rect; + let dir = aData.dir; + let type = aData.type; + let detail = aData.detail; + + this._anchor = aBrowser.ownerGlobal.gBrowser.popupAnchor; + this._anchor.left = rect.left; + this._anchor.top = rect.top; + this._anchor.width = rect.width; + this._anchor.height = rect.height; + this._anchor.hidden = false; + + debug("Opening picker with details: " + JSON.stringify(detail)); + + let window = aBrowser.ownerDocument.defaultView; + let tabbrowser = window.gBrowser; + if (Services.focus.activeWindow != window || + tabbrowser.selectedBrowser != aBrowser) { + // We were sent a message from a window or tab that went into the + // background, so we'll ignore it for now. + return; + } + + this.weakBrowser = Cu.getWeakReference(aBrowser); + this.picker = aBrowser.dateTimePicker; + if (!this.picker) { + debug("aBrowser.dateTimePicker not found, exiting now."); + return; + } + this.picker.loadPicker(type, detail); + // The arrow panel needs an anchor to work. The popupAnchor (this._anchor) + // is a transparent div that the arrow can point to. + this.picker.openPopup(this._anchor, "after_start", rect.left, rect.top); + + this.addPickerListeners(); + }, + + // Picker is closed, do some cleanup. + close: function() { + this.removePickerListeners(); + this.picker = null; + this.weakBrowser = null; + this._anchor.hidden = true; + }, + + // Listen to picker's event. + addPickerListeners: function() { + if (!this.picker) { + return; + } + this.picker.addEventListener("popuphidden", this); + this.picker.addEventListener("DateTimePickerValueChanged", this); + }, + + // Stop listening to picker's event. + removePickerListeners: function() { + if (!this.picker) { + return; + } + this.picker.removeEventListener("popuphidden", this); + this.picker.removeEventListener("DateTimePickerValueChanged", this); + }, +}; diff --git a/toolkit/modules/DeferredTask.jsm b/toolkit/modules/DeferredTask.jsm new file mode 100644 index 000000000..f13c71f53 --- /dev/null +++ b/toolkit/modules/DeferredTask.jsm @@ -0,0 +1,301 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DeferredTask", +]; + +/** + * Sets up a function or an asynchronous task whose execution can be triggered + * after a defined delay. Multiple attempts to run the task before the delay + * has passed are coalesced. The task cannot be re-entered while running, but + * can be executed again after a previous run finished. + * + * A common use case occurs when a data structure should be saved into a file + * every time the data changes, using asynchronous calls, and multiple changes + * to the data may happen within a short time: + * + * let saveDeferredTask = new DeferredTask(function* () { + * yield OS.File.writeAtomic(...); + * // Any uncaught exception will be reported. + * }, 2000); + * + * // The task is ready, but will not be executed until requested. + * + * The "arm" method can be used to start the internal timer that will result in + * the eventual execution of the task. Multiple attempts to arm the timer don't + * introduce further delays: + * + * saveDeferredTask.arm(); + * + * // The task will be executed in 2 seconds from now. + * + * yield waitOneSecond(); + * saveDeferredTask.arm(); + * + * // The task will be executed in 1 second from now. + * + * The timer can be disarmed to reset the delay, or just to cancel execution: + * + * saveDeferredTask.disarm(); + * saveDeferredTask.arm(); + * + * // The task will be executed in 2 seconds from now. + * + * When the internal timer fires and the execution of the task starts, the task + * cannot be canceled anymore. It is however possible to arm the timer again + * during the execution of the task, in which case the task will need to finish + * before the timer is started again, thus guaranteeing a time of inactivity + * between executions that is at least equal to the provided delay. + * + * The "finalize" method can be used to ensure that the task terminates + * properly. The promise it returns is resolved only after the last execution + * of the task is finished. To guarantee that the task is executed for the + * last time, the method prevents any attempt to arm the timer again. + * + * If the timer is already armed when the "finalize" method is called, then the + * task is executed immediately. If the task was already running at this point, + * then one last execution from start to finish will happen again, immediately + * after the current execution terminates. If the timer is not armed, the + * "finalize" method only ensures that any running task terminates. + * + * For example, during shutdown, you may want to ensure that any pending write + * is processed, using the latest version of the data if the timer is armed: + * + * AsyncShutdown.profileBeforeChange.addBlocker( + * "Example service: shutting down", + * () => saveDeferredTask.finalize() + * ); + * + * Instead, if you are going to delete the saved data from disk anyways, you + * might as well prevent any pending write from starting, while still ensuring + * that any write that is currently in progress terminates, so that the file is + * not in use anymore: + * + * saveDeferredTask.disarm(); + * saveDeferredTask.finalize().then(() => OS.File.remove(...)) + * .then(null, Components.utils.reportError); + */ + +// Globals + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer", + "initWithCallback"); + +// DeferredTask + +/** + * Sets up a task whose execution can be triggered after a delay. + * + * @param aTaskFn + * Function or generator function to execute. This argument is passed to + * the "Task.spawn" method every time the task should be executed. This + * task is never re-entered while running. + * @param aDelayMs + * Time between executions, in milliseconds. Multiple attempts to run + * the task before the delay has passed are coalesced. This time of + * inactivity is guaranteed to pass between multiple executions of the + * task, except on finalization, when the task may restart immediately + * after the previous execution finished. + */ +this.DeferredTask = function (aTaskFn, aDelayMs) { + this._taskFn = aTaskFn; + this._delayMs = aDelayMs; +} + +this.DeferredTask.prototype = { + /** + * Function or generator function to execute. + */ + _taskFn: null, + + /** + * Time between executions, in milliseconds. + */ + _delayMs: null, + + /** + * Indicates whether the task is currently requested to start again later, + * regardless of whether it is currently running. + */ + get isArmed() { + return this._armed; + }, + _armed: false, + + /** + * Indicates whether the task is currently running. This is always true when + * read from code inside the task function, but can also be true when read + * from external code, in case the task is an asynchronous generator function. + */ + get isRunning() { + return !!this._runningPromise; + }, + + /** + * Promise resolved when the current execution of the task terminates, or null + * if the task is not currently running. + */ + _runningPromise: null, + + /** + * nsITimer used for triggering the task after a delay, or null in case the + * task is running or there is no task scheduled for execution. + */ + _timer: null, + + /** + * Actually starts the timer with the delay specified on construction. + */ + _startTimer: function () + { + this._timer = new Timer(this._timerCallback.bind(this), this._delayMs, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Requests the execution of the task after the delay specified on + * construction. Multiple calls don't introduce further delays. If the task + * is running, the delay will start when the current execution finishes. + * + * The task will always be executed on a different tick of the event loop, + * even if the delay specified on construction is zero. Multiple "arm" calls + * within the same tick of the event loop are guaranteed to result in a single + * execution of the task. + * + * @note By design, this method doesn't provide a way for the caller to detect + * when the next execution terminates, or collect a result. In fact, + * doing that would often result in duplicate processing or logging. If + * a special operation or error logging is needed on completion, it can + * be better handled from within the task itself, for example using a + * try/catch/finally clause in the task. The "finalize" method can be + * used in the common case of waiting for completion on shutdown. + */ + arm: function () + { + if (this._finalized) { + throw new Error("Unable to arm timer, the object has been finalized."); + } + + this._armed = true; + + // In case the timer callback is running, do not create the timer now, + // because this will be handled by the timer callback itself. Also, the + // timer is not restarted in case it is already running. + if (!this._runningPromise && !this._timer) { + this._startTimer(); + } + }, + + /** + * Cancels any request for a delayed the execution of the task, though the + * task itself cannot be canceled in case it is already running. + * + * This method stops any currently running timer, thus the delay will restart + * from its original value in case the "arm" method is called again. + */ + disarm: function () { + this._armed = false; + if (this._timer) { + // Calling the "cancel" method and discarding the timer reference makes + // sure that the timer callback will not be called later, even if the + // timer thread has already posted the timer event on the main thread. + this._timer.cancel(); + this._timer = null; + } + }, + + /** + * Ensures that any pending task is executed from start to finish, while + * preventing any attempt to arm the timer again. + * + * - If the task is running and the timer is armed, then one last execution + * from start to finish will happen again, immediately after the current + * execution terminates, then the returned promise will be resolved. + * - If the task is running and the timer is not armed, the returned promise + * will be resolved when the current execution terminates. + * - If the task is not running and the timer is armed, then the task is + * started immediately, and the returned promise resolves when the new + * execution terminates. + * - If the task is not running and the timer is not armed, the method returns + * a resolved promise. + * + * @return {Promise} + * @resolves After the last execution of the task is finished. + * @rejects Never. + */ + finalize: function () { + if (this._finalized) { + throw new Error("The object has been already finalized."); + } + this._finalized = true; + + // If the timer is armed, it means that the task is not running but it is + // scheduled for execution. Cancel the timer and run the task immediately. + if (this._timer) { + this.disarm(); + this._timerCallback(); + } + + // Wait for the operation to be completed, or resolve immediately. + if (this._runningPromise) { + return this._runningPromise; + } + return Promise.resolve(); + }, + _finalized: false, + + /** + * Timer callback used to run the delayed task. + */ + _timerCallback: function () + { + let runningDeferred = Promise.defer(); + + // All these state changes must occur at the same time directly inside the + // timer callback, to prevent race conditions and to ensure that all the + // methods behave consistently even if called from inside the task. This + // means that the assignment of "this._runningPromise" must complete before + // the task gets a chance to start. + this._timer = null; + this._armed = false; + this._runningPromise = runningDeferred.promise; + + runningDeferred.resolve(Task.spawn(function* () { + // Execute the provided function asynchronously. + yield Task.spawn(this._taskFn).then(null, Cu.reportError); + + // Now that the task has finished, we check the state of the object to + // determine if we should restart the task again. + if (this._armed) { + if (!this._finalized) { + this._startTimer(); + } else { + // Execute the task again immediately, for the last time. The isArmed + // property should return false while the task is running, and should + // remain false after the last execution terminates. + this._armed = false; + yield Task.spawn(this._taskFn).then(null, Cu.reportError); + } + } + + // Indicate that the execution of the task has finished. This happens + // synchronously with the previous state changes in the function. + this._runningPromise = null; + }.bind(this)).then(null, Cu.reportError)); + }, +}; diff --git a/toolkit/modules/Deprecated.jsm b/toolkit/modules/Deprecated.jsm new file mode 100644 index 000000000..7491a4938 --- /dev/null +++ b/toolkit/modules/Deprecated.jsm @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ "Deprecated" ]; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const PREF_DEPRECATION_WARNINGS = "devtools.errorconsole.deprecation_warnings"; + +Cu.import("resource://gre/modules/Services.jsm"); + +// A flag that indicates whether deprecation warnings should be logged. +var logWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); + +Services.prefs.addObserver(PREF_DEPRECATION_WARNINGS, + function (aSubject, aTopic, aData) { + logWarnings = Services.prefs.getBoolPref(PREF_DEPRECATION_WARNINGS); + }, false); + +/** + * Build a callstack log message. + * + * @param nsIStackFrame aStack + * A callstack to be converted into a string log message. + */ +function stringifyCallstack (aStack) { + // If aStack is invalid, use Components.stack (ignoring the last frame). + if (!aStack || !(aStack instanceof Ci.nsIStackFrame)) { + aStack = Components.stack.caller; + } + + let frame = aStack.caller; + let msg = ""; + // Get every frame in the callstack. + while (frame) { + msg += frame.filename + " " + frame.lineNumber + + " " + frame.name + "\n"; + frame = frame.caller; + } + return msg; +} + +this.Deprecated = { + /** + * Log a deprecation warning. + * + * @param string aText + * Deprecation warning text. + * @param string aUrl + * A URL pointing to documentation describing deprecation + * and the way to address it. + * @param nsIStackFrame aStack + * An optional callstack. If it is not provided a + * snapshot of the current JavaScript callstack will be + * logged. + */ + warning: function (aText, aUrl, aStack) { + if (!logWarnings) { + return; + } + + // If URL is not provided, report an error. + if (!aUrl) { + Cu.reportError("Error in Deprecated.warning: warnings must " + + "provide a URL documenting this deprecation."); + return; + } + + let textMessage = "DEPRECATION WARNING: " + aText + + "\nYou may find more details about this deprecation at: " + + aUrl + "\n" + + // Append a callstack part to the deprecation message. + stringifyCallstack(aStack); + + // Report deprecation warning. + Cu.reportError(textMessage); + } +}; diff --git a/toolkit/modules/FileUtils.jsm b/toolkit/modules/FileUtils.jsm new file mode 100644 index 000000000..df17d60a2 --- /dev/null +++ b/toolkit/modules/FileUtils.jsm @@ -0,0 +1,176 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = [ "FileUtils" ]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + +XPCOMUtils.defineLazyServiceGetter(this, "gDirService", + "@mozilla.org/file/directory_service;1", + "nsIProperties"); + +this.FileUtils = { + MODE_RDONLY : 0x01, + MODE_WRONLY : 0x02, + MODE_RDWR : 0x04, + MODE_CREATE : 0x08, + MODE_APPEND : 0x10, + MODE_TRUNCATE : 0x20, + + PERMS_FILE : 0o644, + PERMS_DIRECTORY : 0o755, + + /** + * Gets a file at the specified hierarchy under a nsIDirectoryService key. + * @param key + * The Directory Service Key to start from + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key|. The last item in this array must be the + * leaf name of a file. + * @return nsIFile object for the file specified. The file is NOT created + * if it does not exist, however all required directories along + * the way are. + */ + getFile: function FileUtils_getFile(key, pathArray, followLinks) { + var file = this.getDir(key, pathArray.slice(0, -1), true, followLinks); + file.append(pathArray[pathArray.length - 1]); + return file; + }, + + /** + * Gets a directory at the specified hierarchy under a nsIDirectoryService + * key. + * @param key + * The Directory Service Key to start from + * @param pathArray + * An array of path components to locate beneath the directory + * specified by |key| + * @param shouldCreate + * true if the directory hierarchy specified in |pathArray| + * should be created if it does not exist, false otherwise. + * @param followLinks (optional) + * true if links should be followed, false otherwise. + * @return nsIFile object for the location specified. + */ + getDir: function FileUtils_getDir(key, pathArray, shouldCreate, followLinks) { + var dir = gDirService.get(key, Ci.nsIFile); + for (var i = 0; i < pathArray.length; ++i) { + dir.append(pathArray[i]); + } + + if (shouldCreate) { + try { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, this.PERMS_DIRECTORY); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) { + throw ex; + } + // Ignore the exception due to a directory that already exists. + } + } + + if (!followLinks) + dir.followLinks = false; + return dir; + }, + + /** + * Opens a file output stream for writing. + * @param file + * The file to write to. + * @param modeFlags + * (optional) File open flags. Can be undefined. + * @returns nsIFileOutputStream to write to. + * @note The stream is initialized with the DEFER_OPEN behavior flag. + * See nsIFileOutputStream. + */ + openFileOutputStream: function FileUtils_openFileOutputStream(file, modeFlags) { + var fos = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + return this._initFileOutputStream(fos, file, modeFlags); + }, + + /** + * Opens an atomic file output stream for writing. + * @param file + * The file to write to. + * @param modeFlags + * (optional) File open flags. Can be undefined. + * @returns nsIFileOutputStream to write to. + * @note The stream is initialized with the DEFER_OPEN behavior flag. + * See nsIFileOutputStream. + * OpeanAtomicFileOutputStream is generally better than openSafeFileOutputStream + * baecause flushing is not needed in most of the issues. + */ + openAtomicFileOutputStream: function FileUtils_openAtomicFileOutputStream(file, modeFlags) { + var fos = Cc["@mozilla.org/network/atomic-file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + return this._initFileOutputStream(fos, file, modeFlags); + }, + + /** + * Opens a safe file output stream for writing. + * @param file + * The file to write to. + * @param modeFlags + * (optional) File open flags. Can be undefined. + * @returns nsIFileOutputStream to write to. + * @note The stream is initialized with the DEFER_OPEN behavior flag. + * See nsIFileOutputStream. + */ + openSafeFileOutputStream: function FileUtils_openSafeFileOutputStream(file, modeFlags) { + var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + return this._initFileOutputStream(fos, file, modeFlags); + }, + + _initFileOutputStream: function FileUtils__initFileOutputStream(fos, file, modeFlags) { + if (modeFlags === undefined) + modeFlags = this.MODE_WRONLY | this.MODE_CREATE | this.MODE_TRUNCATE; + fos.init(file, modeFlags, this.PERMS_FILE, fos.DEFER_OPEN); + return fos; + }, + + /** + * Closes an atomic file output stream. + * @param stream + * The stream to close. + */ + closeAtomicFileOutputStream: function FileUtils_closeAtomicFileOutputStream(stream) { + if (stream instanceof Ci.nsISafeOutputStream) { + try { + stream.finish(); + return; + } + catch (e) { + } + } + stream.close(); + }, + + /** + * Closes a safe file output stream. + * @param stream + * The stream to close. + */ + closeSafeFileOutputStream: function FileUtils_closeSafeFileOutputStream(stream) { + if (stream instanceof Ci.nsISafeOutputStream) { + try { + stream.finish(); + return; + } + catch (e) { + } + } + stream.close(); + }, + + File: Components.Constructor("@mozilla.org/file/local;1", Ci.nsILocalFile, "initWithPath") +}; diff --git a/toolkit/modules/Finder.jsm b/toolkit/modules/Finder.jsm new file mode 100644 index 000000000..c2a9af5b1 --- /dev/null +++ b/toolkit/modules/Finder.jsm @@ -0,0 +1,639 @@ +// vim: set ts=2 sw=2 sts=2 tw=80: +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +this.EXPORTED_SYMBOLS = ["Finder", "GetClipboardSearchString"]; + +const { interfaces: Ci, classes: Cc, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Geometry.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService", + "@mozilla.org/intl/texttosuburi;1", + "nsITextToSubURI"); +XPCOMUtils.defineLazyServiceGetter(this, "Clipboard", + "@mozilla.org/widget/clipboard;1", + "nsIClipboard"); +XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper"); + +const kSelectionMaxLen = 150; +const kMatchesCountLimitPref = "accessibility.typeaheadfind.matchesCountLimit"; + +function Finder(docShell) { + this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind); + this._fastFind.init(docShell); + + this._currentFoundRange = null; + this._docShell = docShell; + this._listeners = []; + this._previousLink = null; + this._searchString = null; + this._highlighter = null; + + docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + .addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + BrowserUtils.getRootWindow(this._docShell).addEventListener("unload", + this.onLocationChange.bind(this, { isTopLevel: true })); +} + +Finder.prototype = { + get iterator() { + if (this._iterator) + return this._iterator; + this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator; + return this._iterator; + }, + + destroy: function() { + if (this._iterator) + this._iterator.reset(); + let window = this._getWindow(); + if (this._highlighter && window) { + // if we clear all the references before we hide the highlights (in both + // highlighting modes), we simply can't use them to find the ranges we + // need to clear from the selection. + this._highlighter.hide(window); + this._highlighter.clear(window); + } + this.listeners = []; + this._docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + .removeProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + this._listeners = []; + this._currentFoundRange = this._fastFind = this._docShell = this._previousLink = + this._highlighter = null; + }, + + addResultListener: function (aListener) { + if (this._listeners.indexOf(aListener) === -1) + this._listeners.push(aListener); + }, + + removeResultListener: function (aListener) { + this._listeners = this._listeners.filter(l => l != aListener); + }, + + _notify: function (options) { + if (typeof options.storeResult != "boolean") + options.storeResult = true; + + if (options.storeResult) { + this._searchString = options.searchString; + this.clipboardSearchString = options.searchString + } + + let foundLink = this._fastFind.foundLink; + let linkURL = null; + if (foundLink) { + let docCharset = null; + let ownerDoc = foundLink.ownerDocument; + if (ownerDoc) + docCharset = ownerDoc.characterSet; + + linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href); + } + + options.linkURL = linkURL; + options.rect = this._getResultRect(); + options.searchString = this._searchString; + + if (!this.iterator.continueRunning({ + caseSensitive: this._fastFind.caseSensitive, + entireWord: this._fastFind.entireWord, + linksOnly: options.linksOnly, + word: options.searchString + })) { + this.iterator.stop(); + } + + this.highlighter.update(options); + this.requestMatchesCount(options.searchString, options.linksOnly); + + this._outlineLink(options.drawOutline); + + for (let l of this._listeners) { + try { + l.onFindResult(options); + } catch (ex) {} + } + }, + + get searchString() { + if (!this._searchString && this._fastFind.searchString) + this._searchString = this._fastFind.searchString; + return this._searchString; + }, + + get clipboardSearchString() { + return GetClipboardSearchString(this._getWindow() + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext)); + }, + + set clipboardSearchString(aSearchString) { + if (!aSearchString || !Clipboard.supportsFindClipboard()) + return; + + ClipboardHelper.copyStringToClipboard(aSearchString, + Ci.nsIClipboard.kFindClipboard); + }, + + set caseSensitive(aSensitive) { + if (this._fastFind.caseSensitive === aSensitive) + return; + this._fastFind.caseSensitive = aSensitive; + this.iterator.reset(); + }, + + set entireWord(aEntireWord) { + if (this._fastFind.entireWord === aEntireWord) + return; + this._fastFind.entireWord = aEntireWord; + this.iterator.reset(); + }, + + get highlighter() { + if (this._highlighter) + return this._highlighter; + + const {FinderHighlighter} = Cu.import("resource://gre/modules/FinderHighlighter.jsm", {}); + return this._highlighter = new FinderHighlighter(this); + }, + + get matchesCountLimit() { + if (typeof this._matchesCountLimit == "number") + return this._matchesCountLimit; + + this._matchesCountLimit = Services.prefs.getIntPref(kMatchesCountLimitPref) || 0; + return this._matchesCountLimit; + }, + + _lastFindResult: null, + + /** + * Used for normal search operations, highlights the first match. + * + * @param aSearchString String to search for. + * @param aLinksOnly Only consider nodes that are links for the search. + * @param aDrawOutline Puts an outline around matched links. + */ + fastFind: function (aSearchString, aLinksOnly, aDrawOutline) { + this._lastFindResult = this._fastFind.find(aSearchString, aLinksOnly); + let searchString = this._fastFind.searchString; + this._notify({ + searchString, + result: this._lastFindResult, + findBackwards: false, + findAgain: false, + drawOutline: aDrawOutline, + linksOnly: aLinksOnly + }); + }, + + /** + * Repeat the previous search. Should only be called after a previous + * call to Finder.fastFind. + * + * @param aFindBackwards Controls the search direction: + * true: before current match, false: after current match. + * @param aLinksOnly Only consider nodes that are links for the search. + * @param aDrawOutline Puts an outline around matched links. + */ + findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) { + this._lastFindResult = this._fastFind.findAgain(aFindBackwards, aLinksOnly); + let searchString = this._fastFind.searchString; + this._notify({ + searchString, + result: this._lastFindResult, + findBackwards: aFindBackwards, + findAgain: true, + drawOutline: aDrawOutline, + linksOnly: aLinksOnly + }); + }, + + /** + * Forcibly set the search string of the find clipboard to the currently + * selected text in the window, on supported platforms (i.e. OSX). + */ + setSearchStringToSelection: function() { + let searchString = this.getActiveSelectionText(); + + // Empty strings are rather useless to search for. + if (!searchString.length) + return null; + + this.clipboardSearchString = searchString; + return searchString; + }, + + highlight: Task.async(function* (aHighlight, aWord, aLinksOnly) { + yield this.highlighter.highlight(aHighlight, aWord, aLinksOnly); + }), + + getInitialSelection: function() { + this._getWindow().setTimeout(() => { + let initialSelection = this.getActiveSelectionText(); + for (let l of this._listeners) { + try { + l.onCurrentSelection(initialSelection, true); + } catch (ex) {} + } + }, 0); + }, + + getActiveSelectionText: function() { + let focusedWindow = {}; + let focusedElement = + Services.focus.getFocusedElementForWindow(this._getWindow(), true, + focusedWindow); + focusedWindow = focusedWindow.value; + + let selText; + + if (focusedElement instanceof Ci.nsIDOMNSEditableElement && + focusedElement.editor) { + // The user may have a selection in an input or textarea. + selText = focusedElement.editor.selectionController + .getSelection(Ci.nsISelectionController.SELECTION_NORMAL) + .toString(); + } else { + // Look for any selected text on the actual page. + selText = focusedWindow.getSelection().toString(); + } + + if (!selText) + return ""; + + // Process our text to get rid of unwanted characters. + selText = selText.trim().replace(/\s+/g, " "); + let truncLength = kSelectionMaxLen; + if (selText.length > truncLength) { + let truncChar = selText.charAt(truncLength).charCodeAt(0); + if (truncChar >= 0xDC00 && truncChar <= 0xDFFF) + truncLength++; + selText = selText.substr(0, truncLength); + } + + return selText; + }, + + enableSelection: function() { + this._fastFind.setSelectionModeAndRepaint(Ci.nsISelectionController.SELECTION_ON); + this._restoreOriginalOutline(); + }, + + removeSelection: function() { + this._fastFind.collapseSelection(); + this.enableSelection(); + this.highlighter.clear(this._getWindow()); + }, + + focusContent: function() { + // Allow Finder listeners to cancel focusing the content. + for (let l of this._listeners) { + try { + if ("shouldFocusContent" in l && + !l.shouldFocusContent()) + return; + } catch (ex) { + Cu.reportError(ex); + } + } + + let fastFind = this._fastFind; + const fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + try { + // Try to find the best possible match that should receive focus and + // block scrolling on focus since find already scrolls. Further + // scrolling is due to user action, so don't override this. + if (fastFind.foundLink) { + fm.setFocus(fastFind.foundLink, fm.FLAG_NOSCROLL); + } else if (fastFind.foundEditable) { + fm.setFocus(fastFind.foundEditable, fm.FLAG_NOSCROLL); + fastFind.collapseSelection(); + } else { + this._getWindow().focus() + } + } catch (e) {} + }, + + onFindbarClose: function() { + this.enableSelection(); + this.highlighter.highlight(false); + this.iterator.reset(); + BrowserUtils.trackToolbarVisibility(this._docShell, "findbar", false); + }, + + onFindbarOpen: function() { + BrowserUtils.trackToolbarVisibility(this._docShell, "findbar", true); + }, + + onModalHighlightChange(useModalHighlight) { + if (this._highlighter) + this._highlighter.onModalHighlightChange(useModalHighlight); + }, + + onHighlightAllChange(highlightAll) { + if (this._highlighter) + this._highlighter.onHighlightAllChange(highlightAll); + if (this._iterator) + this._iterator.reset(); + }, + + keyPress: function (aEvent) { + let controller = this._getSelectionController(this._getWindow()); + + switch (aEvent.keyCode) { + case Ci.nsIDOMKeyEvent.DOM_VK_RETURN: + if (this._fastFind.foundLink) { + let view = this._fastFind.foundLink.ownerDocument.defaultView; + this._fastFind.foundLink.dispatchEvent(new view.MouseEvent("click", { + view: view, + cancelable: true, + bubbles: true, + ctrlKey: aEvent.ctrlKey, + altKey: aEvent.altKey, + shiftKey: aEvent.shiftKey, + metaKey: aEvent.metaKey + })); + } + break; + case Ci.nsIDOMKeyEvent.DOM_VK_TAB: + let direction = Services.focus.MOVEFOCUS_FORWARD; + if (aEvent.shiftKey) { + direction = Services.focus.MOVEFOCUS_BACKWARD; + } + Services.focus.moveFocus(this._getWindow(), null, direction, 0); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: + controller.scrollPage(false); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN: + controller.scrollPage(true); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_UP: + controller.scrollLine(false); + break; + case Ci.nsIDOMKeyEvent.DOM_VK_DOWN: + controller.scrollLine(true); + break; + } + }, + + _notifyMatchesCount: function(result = this._currentMatchesCountResult) { + // The `_currentFound` property is only used for internal bookkeeping. + delete result._currentFound; + result.limit = this.matchesCountLimit; + if (result.total == result.limit) + result.total = -1; + + for (let l of this._listeners) { + try { + l.onMatchesCountResult(result); + } catch (ex) {} + } + + this._currentMatchesCountResult = null; + }, + + requestMatchesCount: function(aWord, aLinksOnly) { + if (this._lastFindResult == Ci.nsITypeAheadFind.FIND_NOTFOUND || + this.searchString == "" || !aWord || !this.matchesCountLimit) { + this._notifyMatchesCount({ + total: 0, + current: 0 + }); + return; + } + + let window = this._getWindow(); + this._currentFoundRange = this._fastFind.getFoundRange(); + + let params = { + caseSensitive: this._fastFind.caseSensitive, + entireWord: this._fastFind.entireWord, + linksOnly: aLinksOnly, + word: aWord + }; + if (!this.iterator.continueRunning(params)) + this.iterator.stop(); + + this.iterator.start(Object.assign(params, { + finder: this, + limit: this.matchesCountLimit, + listener: this, + useCache: true, + })).then(() => { + // Without a valid result, there's nothing to notify about. This happens + // when the iterator was started before and won the race. + if (!this._currentMatchesCountResult || !this._currentMatchesCountResult.total) + return; + this._notifyMatchesCount(); + }); + }, + + // FinderIterator listener implementation + + onIteratorRangeFound(range) { + let result = this._currentMatchesCountResult; + if (!result) + return; + + ++result.total; + if (!result._currentFound) { + ++result.current; + result._currentFound = (this._currentFoundRange && + range.startContainer == this._currentFoundRange.startContainer && + range.startOffset == this._currentFoundRange.startOffset && + range.endContainer == this._currentFoundRange.endContainer && + range.endOffset == this._currentFoundRange.endOffset); + } + }, + + onIteratorReset() {}, + + onIteratorRestart({ word, linksOnly }) { + this.requestMatchesCount(word, linksOnly); + }, + + onIteratorStart() { + this._currentMatchesCountResult = { + total: 0, + current: 0, + _currentFound: false + }; + }, + + _getWindow: function () { + if (!this._docShell) + return null; + return this._docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + }, + + /** + * Get the bounding selection rect in CSS px relative to the origin of the + * top-level content document. + */ + _getResultRect: function () { + let topWin = this._getWindow(); + let win = this._fastFind.currentWindow; + if (!win) + return null; + + let selection = win.getSelection(); + if (!selection.rangeCount || selection.isCollapsed) { + // The selection can be into an input or a textarea element. + let nodes = win.document.querySelectorAll("input, textarea"); + for (let node of nodes) { + if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) { + try { + let sc = node.editor.selectionController; + selection = sc.getSelection(Ci.nsISelectionController.SELECTION_NORMAL); + if (selection.rangeCount && !selection.isCollapsed) { + break; + } + } catch (e) { + // If this textarea is hidden, then its selection controller might + // not be intialized. Ignore the failure. + } + } + } + } + + if (!selection.rangeCount || selection.isCollapsed) { + return null; + } + + let utils = topWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let scrollX = {}, scrollY = {}; + utils.getScrollXY(false, scrollX, scrollY); + + for (let frame = win; frame != topWin; frame = frame.parent) { + let rect = frame.frameElement.getBoundingClientRect(); + let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; + let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; + scrollX.value += rect.left + parseInt(left, 10); + scrollY.value += rect.top + parseInt(top, 10); + } + let rect = Rect.fromRect(selection.getRangeAt(0).getBoundingClientRect()); + return rect.translate(scrollX.value, scrollY.value); + }, + + _outlineLink: function (aDrawOutline) { + let foundLink = this._fastFind.foundLink; + + // Optimization: We are drawing outlines and we matched + // the same link before, so don't duplicate work. + if (foundLink == this._previousLink && aDrawOutline) + return; + + this._restoreOriginalOutline(); + + if (foundLink && aDrawOutline) { + // Backup original outline + this._tmpOutline = foundLink.style.outline; + this._tmpOutlineOffset = foundLink.style.outlineOffset; + + // Draw pseudo focus rect + // XXX Should we change the following style for FAYT pseudo focus? + // XXX Shouldn't we change default design if outline is visible + // already? + // Don't set the outline-color, we should always use initial value. + foundLink.style.outline = "1px dotted"; + foundLink.style.outlineOffset = "0"; + + this._previousLink = foundLink; + } + }, + + _restoreOriginalOutline: function () { + // Removes the outline around the last found link. + if (this._previousLink) { + this._previousLink.style.outline = this._tmpOutline; + this._previousLink.style.outlineOffset = this._tmpOutlineOffset; + this._previousLink = null; + } + }, + + _getSelectionController: function(aWindow) { + // display: none iframes don't have a selection controller, see bug 493658 + try { + if (!aWindow.innerWidth || !aWindow.innerHeight) + return null; + } catch (e) { + // If getting innerWidth or innerHeight throws, we can't get a selection + // controller. + return null; + } + + // Yuck. See bug 138068. + let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + + let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + return controller; + }, + + // Start of nsIWebProgressListener implementation. + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + if (!aWebProgress.isTopLevel) + return; + // Ignore events that don't change the document. + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + return; + + // Avoid leaking if we change the page. + this._lastFindResult = this._previousLink = this._currentFoundRange = null; + this.highlighter.onLocationChange(); + this.iterator.reset(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]) +}; + +function GetClipboardSearchString(aLoadContext) { + let searchString = ""; + if (!Clipboard.supportsFindClipboard()) + return searchString; + + try { + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(aLoadContext); + trans.addDataFlavor("text/unicode"); + + Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard); + + let data = {}; + let dataLen = {}; + trans.getTransferData("text/unicode", data, dataLen); + if (data.value) { + data = data.value.QueryInterface(Ci.nsISupportsString); + searchString = data.toString(); + } + } catch (ex) {} + + return searchString; +} + +this.Finder = Finder; +this.GetClipboardSearchString = GetClipboardSearchString; diff --git a/toolkit/modules/FinderHighlighter.jsm b/toolkit/modules/FinderHighlighter.jsm new file mode 100644 index 000000000..e2079fd37 --- /dev/null +++ b/toolkit/modules/FinderHighlighter.jsm @@ -0,0 +1,1615 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["FinderHighlighter"]; + +const { interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Color", "resource://gre/modules/Color.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); +XPCOMUtils.defineLazyGetter(this, "kDebug", () => { + const kDebugPref = "findbar.modalHighlight.debug"; + return Services.prefs.getPrefType(kDebugPref) && Services.prefs.getBoolPref(kDebugPref); +}); + +const kContentChangeThresholdPx = 5; +const kBrightTextSampleSize = 5; +const kModalHighlightRepaintLoFreqMs = 100; +const kModalHighlightRepaintHiFreqMs = 16; +const kHighlightAllPref = "findbar.highlightAll"; +const kModalHighlightPref = "findbar.modalHighlight"; +const kFontPropsCSS = ["color", "font-family", "font-kerning", "font-size", + "font-size-adjust", "font-stretch", "font-variant", "font-weight", "line-height", + "letter-spacing", "text-emphasis", "text-orientation", "text-transform", "word-spacing"]; +const kFontPropsCamelCase = kFontPropsCSS.map(prop => { + let parts = prop.split("-"); + return parts.shift() + parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(""); +}); +const kRGBRE = /^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*/i; +// This uuid is used to prefix HTML element IDs in order to make them unique and +// hard to clash with IDs content authors come up with. +const kModalIdPrefix = "cedee4d0-74c5-4f2d-ab43-4d37c0f9d463"; +const kModalOutlineId = kModalIdPrefix + "-findbar-modalHighlight-outline"; +const kOutlineBoxColor = "255,197,53"; +const kOutlineBoxBorderSize = 2; +const kOutlineBoxBorderRadius = 3; +const kModalStyles = { + outlineNode: [ + ["background-color", `rgb(${kOutlineBoxColor})`], + ["background-clip", "padding-box"], + ["border", `${kOutlineBoxBorderSize}px solid`], + ["-moz-border-top-colors", `rgba(${kOutlineBoxColor},.1) rgba(${kOutlineBoxColor},.4) rgba(${kOutlineBoxColor},.7)`], + ["-moz-border-right-colors", `rgba(${kOutlineBoxColor},.1) rgba(${kOutlineBoxColor},.4) rgba(${kOutlineBoxColor},.7)`], + ["-moz-border-bottom-colors", `rgba(${kOutlineBoxColor},.1) rgba(${kOutlineBoxColor},.4) rgba(${kOutlineBoxColor},.7)`], + ["-moz-border-left-colors", `rgba(${kOutlineBoxColor},.1) rgba(${kOutlineBoxColor},.4) rgba(${kOutlineBoxColor},.7)`], + ["border-radius", `${kOutlineBoxBorderRadius}px`], + ["box-shadow", `0 ${kOutlineBoxBorderSize}px 0 0 rgba(0,0,0,.1)`], + ["color", "#000"], + ["display", "-moz-box"], + ["margin", `-${kOutlineBoxBorderSize}px 0 0 -${kOutlineBoxBorderSize}px !important`], + ["overflow", "hidden"], + ["pointer-events", "none"], + ["position", "absolute"], + ["white-space", "nowrap"], + ["will-change", "transform"], + ["z-index", 2] + ], + outlineNodeDebug: [ ["z-index", 2147483647] ], + outlineText: [ + ["margin", "0 !important"], + ["padding", "0 !important"], + ["vertical-align", "top !important"] + ], + maskNode: [ + ["background", "rgba(0,0,0,.25)"], + ["pointer-events", "none"], + ["position", "absolute"], + ["z-index", 1] + ], + maskNodeTransition: [ + ["transition", "background .2s ease-in"] + ], + maskNodeDebug: [ + ["z-index", 2147483646], + ["top", 0], + ["left", 0] + ], + maskNodeBrightText: [ ["background", "rgba(255,255,255,.25)"] ] +}; +const kModalOutlineAnim = { + "keyframes": [ + { transform: "scaleX(1) scaleY(1)" }, + { transform: "scaleX(1.5) scaleY(1.5)", offset: .5, easing: "ease-in" }, + { transform: "scaleX(1) scaleY(1)" } + ], + duration: 50, +}; +const kNSHTML = "http://www.w3.org/1999/xhtml"; + +function mockAnonymousContentNode(domNode) { + return { + setTextContentForElement(id, text) { + (domNode.querySelector("#" + id) || domNode).textContent = text; + }, + getAttributeForElement(id, attrName) { + let node = domNode.querySelector("#" + id) || domNode; + if (!node.hasAttribute(attrName)) + return undefined; + return node.getAttribute(attrName); + }, + setAttributeForElement(id, attrName, attrValue) { + (domNode.querySelector("#" + id) || domNode).setAttribute(attrName, attrValue); + }, + removeAttributeForElement(id, attrName) { + let node = domNode.querySelector("#" + id) || domNode; + if (!node.hasAttribute(attrName)) + return; + node.removeAttribute(attrName); + }, + remove() { + try { + domNode.parentNode.removeChild(domNode); + } catch (ex) {} + }, + setAnimationForElement(id, keyframes, duration) { + return (domNode.querySelector("#" + id) || domNode).animate(keyframes, duration); + }, + setCutoutRectsForElement(id, rects) { + // no-op for now. + } + }; +} + +let gWindows = new WeakMap(); + +/** + * FinderHighlighter class that is used by Finder.jsm to take care of the + * 'Highlight All' feature, which can highlight all find occurrences in a page. + * + * @param {Finder} finder Finder.jsm instance + */ +function FinderHighlighter(finder) { + this._highlightAll = Services.prefs.getBoolPref(kHighlightAllPref); + this._modal = Services.prefs.getBoolPref(kModalHighlightPref); + this.finder = finder; +} + +FinderHighlighter.prototype = { + get iterator() { + if (this._iterator) + return this._iterator; + this._iterator = Cu.import("resource://gre/modules/FinderIterator.jsm", null).FinderIterator; + return this._iterator; + }, + + /** + * Each window is unique, globally, and the relation between an active + * highlighting session and a window is 1:1. + * For each window we track a number of properties which _at least_ consist of + * - {Boolean} detectedGeometryChange Whether the geometry of the found ranges' + * rectangles has changed substantially + * - {Set} dynamicRangesSet Set of ranges that may move around, depending + * on page layout changes and user input + * - {Map} frames Collection of frames that were encountered + * when inspecting the found ranges + * - {Map} modalHighlightRectsMap Collection of ranges and their corresponding + * Rects + * + * @param {nsIDOMWindow} window + * @return {Object} + */ + getForWindow(window, propName = null) { + if (!gWindows.has(window)) { + gWindows.set(window, { + detectedGeometryChange: false, + dynamicRangesSet: new Set(), + frames: new Map(), + modalHighlightRectsMap: new Map(), + previousRangeRectsCount: 0 + }); + } + return gWindows.get(window); + }, + + /** + * Notify all registered listeners that the 'Highlight All' operation finished. + * + * @param {Boolean} highlight Whether highlighting was turned on + */ + notifyFinished(highlight) { + for (let l of this.finder._listeners) { + try { + l.onHighlightFinished(highlight); + } catch (ex) {} + } + }, + + /** + * Toggle highlighting all occurrences of a word in a page. This method will + * be called recursively for each (i)frame inside a page. + * + * @param {Booolean} highlight Whether highlighting should be turned on + * @param {String} word Needle to search for and highlight when found + * @param {Boolean} linksOnly Only consider nodes that are links for the search + * @yield {Promise} that resolves once the operation has finished + */ + highlight: Task.async(function* (highlight, word, linksOnly) { + let window = this.finder._getWindow(); + let dict = this.getForWindow(window); + let controller = this.finder._getSelectionController(window); + let doc = window.document; + this._found = false; + + if (!controller || !doc || !doc.documentElement) { + // Without the selection controller, + // we are unable to (un)highlight any matches + return; + } + + if (highlight) { + let params = { + allowDistance: 1, + caseSensitive: this.finder._fastFind.caseSensitive, + entireWord: this.finder._fastFind.entireWord, + linksOnly, word, + finder: this.finder, + listener: this, + useCache: true, + window + }; + if (this.iterator.isAlreadyRunning(params) || + (this._modal && this.iterator._areParamsEqual(params, dict.lastIteratorParams))) { + return; + } + + if (!this._modal) + dict.visible = true; + yield this.iterator.start(params); + if (this._found) { + this.finder._outlineLink(true); + dict.updateAllRanges = true; + } + } else { + this.hide(window); + + // Removing the highlighting always succeeds, so return true. + this._found = true; + } + + this.notifyFinished({ highlight, found: this._found }); + }), + + // FinderIterator listener implementation + + onIteratorRangeFound(range) { + this.highlightRange(range); + this._found = true; + }, + + onIteratorReset() {}, + + onIteratorRestart() { + this.clear(this.finder._getWindow()); + }, + + onIteratorStart(params) { + let window = this.finder._getWindow(); + let dict = this.getForWindow(window); + // Save a clean params set for use later in the `update()` method. + dict.lastIteratorParams = params; + if (!this._modal) + this.hide(window, this.finder._fastFind.getFoundRange()); + this.clear(window); + }, + + /** + * Add a range to the find selection, i.e. highlight it, and if it's inside an + * editable node, track it. + * + * @param {nsIDOMRange} range Range object to be highlighted + */ + highlightRange(range) { + let node = range.startContainer; + let editableNode = this._getEditableNode(node); + let window = node.ownerDocument.defaultView; + let controller = this.finder._getSelectionController(window); + if (editableNode) { + controller = editableNode.editor.selectionController; + } + + if (this._modal) { + this._modalHighlight(range, controller, window); + } else { + let findSelection = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); + findSelection.addRange(range); + // Check if the range is inside an iframe. + if (window != window.top) { + let dict = this.getForWindow(window.top); + if (!dict.frames.has(window)) + dict.frames.set(window, null); + } + } + + if (editableNode) { + // Highlighting added, so cache this editor, and hook up listeners + // to ensure we deal properly with edits within the highlighting + this._addEditorListeners(editableNode.editor); + } + }, + + /** + * If modal highlighting is enabled, show the dimmed background that will overlay + * the page. + * + * @param {nsIDOMWindow} window The dimmed background will overlay this window. + * Optional, defaults to the finder window. + */ + show(window = null) { + window = (window || this.finder._getWindow()).top; + let dict = this.getForWindow(window); + if (!this._modal || dict.visible) + return; + + dict.visible = true; + + this._maybeCreateModalHighlightNodes(window); + this._addModalHighlightListeners(window); + }, + + /** + * Clear all highlighted matches. If modal highlighting is enabled and + * the outline + dimmed background is currently visible, both will be hidden. + * + * @param {nsIDOMWindow} window The dimmed background will overlay this window. + * Optional, defaults to the finder window. + * @param {nsIDOMRange} skipRange A range that should not be removed from the + * find selection. + * @param {nsIDOMEvent} event When called from an event handler, this will + * be the triggering event. + */ + hide(window, skipRange = null, event = null) { + try { + window = window.top; + } catch (ex) { + Cu.reportError(ex); + return; + } + let dict = this.getForWindow(window); + + let isBusySelecting = dict.busySelecting; + dict.busySelecting = false; + // Do not hide on anything but a left-click. + if (event && event.type == "click" && (event.button !== 0 || event.altKey || + event.ctrlKey || event.metaKey || event.shiftKey || event.relatedTarget || + isBusySelecting || (event.target.localName == "a" && event.target.href))) { + return; + } + + this._clearSelection(this.finder._getSelectionController(window), skipRange); + for (let frame of dict.frames.keys()) + this._clearSelection(this.finder._getSelectionController(frame), skipRange); + + // Next, check our editor cache, for editors belonging to this + // document + if (this._editors) { + let doc = window.document; + for (let x = this._editors.length - 1; x >= 0; --x) { + if (this._editors[x].document == doc) { + this._clearSelection(this._editors[x].selectionController, skipRange); + // We don't need to listen to this editor any more + this._unhookListenersAtIndex(x); + } + } + } + + if (dict.modalRepaintScheduler) { + window.clearTimeout(dict.modalRepaintScheduler); + dict.modalRepaintScheduler = null; + } + dict.lastWindowDimensions = null; + + if (dict.modalHighlightOutline) { + dict.modalHighlightOutline.setAttributeForElement(kModalOutlineId, "style", + dict.modalHighlightOutline.getAttributeForElement(kModalOutlineId, "style") + + "; opacity: 0"); + } + + this._removeHighlightAllMask(window); + this._removeModalHighlightListeners(window); + + dict.visible = false; + }, + + /** + * Called by the Finder after a find result comes in; update the position and + * content of the outline to the newly found occurrence. + * To make sure that the outline covers the found range completely, all the + * CSS styles that influence the text are copied and applied to the outline. + * + * @param {Object} data Dictionary coming from Finder that contains the + * following properties: + * {Number} result One of the nsITypeAheadFind.FIND_* constants + * indicating the result of a search operation. + * {Boolean} findBackwards If TRUE, the search was performed backwards, + * FALSE if forwards. + * {Boolean} findAgain If TRUE, the search was performed using the same + * search string as before. + * {String} linkURL If a link was hit, this will contain a URL string. + * {Rect} rect An object with top, left, width and height + * coordinates of the current selection. + * {String} searchString The string the search was performed with. + * {Boolean} storeResult Indicator if the search string should be stored + * by the consumer of the Finder. + */ + update(data) { + let window = this.finder._getWindow(); + let dict = this.getForWindow(window); + let foundRange = this.finder._fastFind.getFoundRange(); + + // Place the match placeholder on top of the current found range. + if (data.result == Ci.nsITypeAheadFind.FIND_NOTFOUND || !data.searchString || !foundRange) { + this.hide(window); + return; + } + + if (!this._modal) { + if (this._highlightAll) { + dict.currentFoundRange = foundRange; + let params = this.iterator.params; + if (dict.visible && this.iterator._areParamsEqual(params, dict.lastIteratorParams)) + return; + if (!dict.visible && !params) + params = {word: data.searchString, linksOnly: data.linksOnly}; + if (params) + this.highlight(true, params.word, params.linksOnly); + } + return; + } + + if (foundRange !== dict.currentFoundRange || data.findAgain) { + dict.currentFoundRange = foundRange; + + let textContent = this._getRangeContentArray(foundRange); + if (!textContent.length) { + this.hide(window); + return; + } + + if (data.findAgain) + dict.updateAllRanges = true; + + if (!dict.visible) + this.show(window); + else + this._maybeCreateModalHighlightNodes(window); + } + + let outlineNode = dict.modalHighlightOutline; + if (outlineNode) { + if (dict.animation) + dict.animation.finish(); + dict.animation = outlineNode.setAnimationForElement(kModalOutlineId, + Cu.cloneInto(kModalOutlineAnim.keyframes, window), kModalOutlineAnim.duration); + dict.animation.onfinish = () => dict.animation = null; + } + + if (this._highlightAll) + this.highlight(true, data.searchString, data.linksOnly); + }, + + /** + * Invalidates the list by clearing the map of highlighted ranges that we + * keep to build the mask for. + */ + clear(window = null) { + if (!window || !window.top) + return; + + let dict = this.getForWindow(window.top); + if (dict.animation) + dict.animation.finish(); + dict.dynamicRangesSet.clear(); + dict.frames.clear(); + dict.modalHighlightRectsMap.clear(); + dict.brightText = null; + }, + + /** + * When the current page is refreshed or navigated away from, the CanvasFrame + * contents is not valid anymore, i.e. all anonymous content is destroyed. + * We need to clear the references we keep, which'll make sure we redraw + * everything when the user starts to find in page again. + */ + onLocationChange() { + let window = this.finder._getWindow(); + if (!window || !window.top) + return; + this.hide(window); + let dict = this.getForWindow(window); + this.clear(window); + dict.currentFoundRange = dict.lastIteratorParams = null; + + if (!dict.modalHighlightOutline) + return; + + if (kDebug) { + dict.modalHighlightOutline.remove(); + } else { + try { + window.document.removeAnonymousContent(dict.modalHighlightOutline); + } catch (ex) {} + } + + dict.modalHighlightOutline = null; + }, + + /** + * When `kModalHighlightPref` pref changed during a session, this callback is + * invoked. When modal highlighting is turned off, we hide the CanvasFrame + * contents. + * + * @param {Boolean} useModalHighlight + */ + onModalHighlightChange(useModalHighlight) { + let window = this.finder._getWindow(); + if (window && this._modal && !useModalHighlight) { + this.hide(window); + this.clear(window); + } + this._modal = useModalHighlight; + }, + + /** + * When 'Highlight All' is toggled during a session, this callback is invoked + * and when it's turned off, the found occurrences will be removed from the mask. + * + * @param {Boolean} highlightAll + */ + onHighlightAllChange(highlightAll) { + this._highlightAll = highlightAll; + if (!highlightAll) { + let window = this.finder._getWindow(); + if (!this._modal) + this.hide(window); + this.clear(window); + this._scheduleRepaintOfMask(window); + } + }, + + /** + * Utility; removes all ranges from the find selection that belongs to a + * controller. Optionally skips a specific range. + * + * @param {nsISelectionController} controller + * @param {nsIDOMRange} restoreRange + */ + _clearSelection(controller, restoreRange = null) { + if (!controller) + return; + let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND); + sel.removeAllRanges(); + if (restoreRange) { + sel = controller.getSelection(Ci.nsISelectionController.SELECTION_NORMAL); + sel.addRange(restoreRange); + controller.setDisplaySelection(Ci.nsISelectionController.SELECTION_ATTENTION); + controller.repaintSelection(Ci.nsISelectionController.SELECTION_NORMAL); + } + }, + + /** + * Utility; get the nsIDOMWindowUtils for a window. + * + * @param {nsIDOMWindow} window Optional, defaults to the finder window. + * @return {nsIDOMWindowUtils} + */ + _getDWU(window = null) { + return (window || this.finder._getWindow()) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + /** + * Utility; returns the bounds of the page relative to the viewport. + * If the pages is part of a frameset or inside an iframe of any kind, its + * offset is accounted for. + * Geometry.jsm takes care of the DOMRect calculations. + * + * @param {nsIDOMWindow} window Window to read the boundary rect from + * @param {Boolean} [includeScroll] Whether to ignore the scroll offset, + * which is useful for comparing DOMRects. + * Optional, defaults to `true` + * @return {Rect} + */ + _getRootBounds(window, includeScroll = true) { + let dwu = this._getDWU(window.top); + let cssPageRect = Rect.fromRect(dwu.getRootBounds()); + let scrollX = {}; + let scrollY = {}; + if (includeScroll && window == window.top) { + dwu.getScrollXY(false, scrollX, scrollY); + cssPageRect.translate(scrollX.value, scrollY.value); + } + + // If we're in a frame, update the position of the rect (top/ left). + let currWin = window; + while (currWin != window.top) { + // Since the frame is an element inside a parent window, we'd like to + // learn its position relative to it. + let el = this._getDWU(currWin).containerElement; + currWin = currWin.parent; + dwu = this._getDWU(currWin); + let parentRect = Rect.fromRect(dwu.getBoundsWithoutFlushing(el)); + + if (includeScroll) { + dwu.getScrollXY(false, scrollX, scrollY); + parentRect.translate(scrollX.value, scrollY.value); + // If the current window is an iframe with scrolling="no" and its parent + // is also an iframe the scroll offsets from the parents' documentElement + // (inverse scroll position) needs to be subtracted from the parent + // window rect. + if (el.getAttribute("scrolling") == "no" && currWin != window.top) { + let docEl = currWin.document.documentElement; + parentRect.translate(-docEl.scrollLeft, -docEl.scrollTop); + } + } + + cssPageRect.translate(parentRect.left, parentRect.top); + } + + return cssPageRect; + }, + + /** + * Utility; fetch the full width and height of the current window, excluding + * scrollbars. + * + * @param {nsiDOMWindow} window The current finder window. + * @return {Object} The current full page dimensions with `width` and `height` + * properties + */ + _getWindowDimensions(window) { + // First we'll try without flushing layout, because it's way faster. + let dwu = this._getDWU(window); + let { width, height } = dwu.getRootBounds(); + + if (!width || !height) { + // We need a flush after all :'( + width = window.innerWidth + window.scrollMaxX - window.scrollMinX; + height = window.innerHeight + window.scrollMaxY - window.scrollMinY; + + let scrollbarHeight = {}; + let scrollbarWidth = {}; + dwu.getScrollbarSize(false, scrollbarWidth, scrollbarHeight); + width -= scrollbarWidth.value; + height -= scrollbarHeight.value; + } + + return { width, height }; + }, + + /** + * Utility; fetch the current text contents of a given range. + * + * @param {nsIDOMRange} range Range object to extract the contents from. + * @return {Array} Snippets of text. + */ + _getRangeContentArray(range) { + let content = range.cloneContents(); + let textContent = []; + for (let node of content.childNodes) { + textContent.push(node.textContent || node.nodeValue); + } + return textContent; + }, + + /** + * Utility; get all available font styles as applied to the content of a given + * range. The CSS properties we look for can be found in `kFontPropsCSS`. + * + * @param {nsIDOMRange} range Range to fetch style info from. + * @return {Object} Dictionary consisting of the styles that were found. + */ + _getRangeFontStyle(range) { + let node = range.startContainer; + while (node.nodeType != 1) + node = node.parentNode; + let style = node.ownerDocument.defaultView.getComputedStyle(node, ""); + let props = {}; + for (let prop of kFontPropsCamelCase) { + if (prop in style && style[prop]) + props[prop] = style[prop]; + } + return props; + }, + + /** + * Utility; transform a dictionary object as returned by `_getRangeFontStyle` + * above into a HTML style attribute value. + * + * @param {Object} fontStyle + * @return {String} + */ + _getHTMLFontStyle(fontStyle) { + let style = []; + for (let prop of Object.getOwnPropertyNames(fontStyle)) { + let idx = kFontPropsCamelCase.indexOf(prop); + if (idx == -1) + continue; + style.push(`${kFontPropsCSS[idx]}: ${fontStyle[prop]}`); + } + return style.join("; "); + }, + + /** + * Transform a style definition array as defined in `kModalStyles` into a CSS + * string that can be used to set the 'style' property of a DOM node. + * + * @param {Array} stylePairs Two-dimensional array of style pairs + * @param {...Array} [additionalStyles] Optional set of style pairs that will + * augment or override the styles defined + * by `stylePairs` + * @return {String} + */ + _getStyleString(stylePairs, ...additionalStyles) { + let baseStyle = new Map(stylePairs); + for (let additionalStyle of additionalStyles) { + for (let [prop, value] of additionalStyle) + baseStyle.set(prop, value); + } + return [...baseStyle].map(([cssProp, cssVal]) => `${cssProp}: ${cssVal}`).join("; "); + }, + + /** + * Checks whether a CSS RGB color value can be classified as being 'bright'. + * + * @param {String} cssColor RGB color value in the default format rgb[a](r,g,b) + * @return {Boolean} + */ + _isColorBright(cssColor) { + cssColor = cssColor.match(kRGBRE); + if (!cssColor || !cssColor.length) + return false; + cssColor.shift(); + return new Color(...cssColor).isBright; + }, + + /** + * Detects if the overall text color in the page can be described as bright. + * This is done according to the following algorithm: + * 1. With the entire set of ranges that we have found thusfar; + * 2. Get an odd-numbered `sampleSize`, with a maximum of `kBrightTextSampleSize` + * ranges, + * 3. Slice the set of ranges into `sampleSize` number of equal parts, + * 4. Grab the first range for each slice and inspect the brightness of the + * color of its text content. + * 5. When the majority of ranges are counted as contain bright colored text, + * the page is considered to contain bright text overall. + * + * @param {Object} dict Dictionary of properties belonging to the + * currently active window. The page text color property + * will be recorded in `dict.brightText` as `true` or `false`. + */ + _detectBrightText(dict) { + let sampleSize = Math.min(dict.modalHighlightRectsMap.size, kBrightTextSampleSize); + let ranges = [...dict.modalHighlightRectsMap.keys()]; + let rangesCount = ranges.length; + // Make sure the sample size is an odd number. + if (sampleSize % 2 == 0) { + // Make the currently found range weigh heavier. + if (dict.currentFoundRange) { + ranges.push(dict.currentFoundRange); + ++sampleSize; + ++rangesCount; + } else { + --sampleSize; + } + } + let brightCount = 0; + for (let i = 0; i < sampleSize; ++i) { + let range = ranges[Math.floor((rangesCount / sampleSize) * i)]; + let fontStyle = this._getRangeFontStyle(range); + if (this._isColorBright(fontStyle.color)) + ++brightCount; + } + + dict.brightText = (brightCount >= Math.ceil(sampleSize / 2)); + }, + + /** + * Checks if a range is inside a DOM node that's positioned in a way that it + * doesn't scroll along when the document is scrolled and/ or zoomed. This + * is the case for 'fixed' and 'sticky' positioned elements, elements inside + * (i)frames and elements that have their overflow styles set to 'auto' or + * 'scroll'. + * + * @param {nsIDOMRange} range Range that be enclosed in a dynamic container + * @return {Boolean} + */ + _isInDynamicContainer(range) { + const kFixed = new Set(["fixed", "sticky", "scroll", "auto"]); + let node = range.startContainer; + while (node.nodeType != 1) + node = node.parentNode; + let document = node.ownerDocument; + let window = document.defaultView; + let dict = this.getForWindow(window.top); + + // Check if we're in a frameset (including iframes). + if (window != window.top) { + if (!dict.frames.has(window)) + dict.frames.set(window, null); + return true; + } + + do { + let style = window.getComputedStyle(node, null); + if (kFixed.has(style.position) || kFixed.has(style.overflow) || + kFixed.has(style.overflowX) || kFixed.has(style.overflowY)) { + return true; + } + node = node.parentNode; + } while (node && node != document.documentElement) + + return false; + }, + + /** + * Read and store the rectangles that encompass the entire region of a range + * for use by the drawing function of the highlighter. + * + * @param {nsIDOMRange} range Range to fetch the rectangles from + * @param {Object} [dict] Dictionary of properties belonging to + * the currently active window + * @return {Set} Set of rects that were found for the range + */ + _getRangeRects(range, dict = null) { + let window = range.startContainer.ownerDocument.defaultView; + let bounds; + // If the window is part of a frameset, try to cache the bounds query. + if (dict && dict.frames.has(window)) { + bounds = dict.frames.get(window); + if (!bounds) { + bounds = this._getRootBounds(window); + dict.frames.set(window, bounds); + } + } else + bounds = this._getRootBounds(window); + + let topBounds = this._getRootBounds(window.top, false); + let rects = []; + // A range may consist of multiple rectangles, we can also do these kind of + // precise cut-outs. range.getBoundingClientRect() returns the fully + // encompassing rectangle, which is too much for our purpose here. + for (let rect of range.getClientRects()) { + rect = Rect.fromRect(rect); + rect.x += bounds.x; + rect.y += bounds.y; + // If the rect is not even visible from the top document, we can ignore it. + if (rect.intersects(topBounds)) + rects.push(rect); + } + return rects; + }, + + /** + * Read and store the rectangles that encompass the entire region of a range + * for use by the drawing function of the highlighter and store them in the + * cache. + * + * @param {nsIDOMRange} range Range to fetch the rectangles from + * @param {Boolean} [checkIfDynamic] Whether we should check if the range + * is dynamic as per the rules in + * `_isInDynamicContainer()`. Optional, + * defaults to `true` + * @param {Object} [dict] Dictionary of properties belonging to + * the currently active window + * @return {Set} Set of rects that were found for the range + */ + _updateRangeRects(range, checkIfDynamic = true, dict = null) { + let window = range.startContainer.ownerDocument.defaultView; + let rects = this._getRangeRects(range, dict); + + // Only fetch the rect at this point, if not passed in as argument. + dict = dict || this.getForWindow(window.top); + let oldRects = dict.modalHighlightRectsMap.get(range); + dict.modalHighlightRectsMap.set(range, rects); + // Check here if we suddenly went down to zero rects from more than zero before, + // which indicates that we should re-iterate the document. + if (oldRects && oldRects.length && !rects.length) + dict.detectedGeometryChange = true; + if (checkIfDynamic && this._isInDynamicContainer(range)) + dict.dynamicRangesSet.add(range); + return rects; + }, + + /** + * Re-read the rectangles of the ranges that we keep track of separately, + * because they're enclosed by a position: fixed container DOM node or (i)frame. + * + * @param {Object} dict Dictionary of properties belonging to the currently + * active window + */ + _updateDynamicRangesRects(dict) { + // Reset the frame bounds cache. + for (let frame of dict.frames.keys()) + dict.frames.set(frame, null); + for (let range of dict.dynamicRangesSet) + this._updateRangeRects(range, false, dict); + }, + + /** + * Update the content, position and style of the yellow current found range + * outline that floats atop the mask with the dimmed background. + * Rebuild it, if necessary, This will deactivate the animation between + * occurrences. + * + * @param {Object} dict Dictionary of properties belonging to the + * currently active window + * @param {Array} [textContent] Array of text that's inside the range. Optional, + * defaults to `null` + * @param {Object} [fontStyle] Dictionary of CSS styles in camelCase as + * returned by `_getRangeFontStyle()`. Optional + */ + _updateRangeOutline(dict, textContent = null, fontStyle = null) { + let range = dict.currentFoundRange; + if (!range) + return; + + fontStyle = fontStyle || this._getRangeFontStyle(range); + // Text color in the outline is determined by kModalStyles. + delete fontStyle.color; + + let rects = this._getRangeRects(range); + textContent = textContent || this._getRangeContentArray(range); + + let outlineAnonNode = dict.modalHighlightOutline; + let rectCount = rects.length; + // (re-)Building the outline is conditional and happens when one of the + // following conditions is met: + // 1. No outline nodes were built before, or + // 2. When the amount of rectangles to draw is different from before, or + // 3. When there's more than one rectangle to draw, because it's impossible + // to animate that consistently with AnonymousContent nodes. + let rebuildOutline = (!outlineAnonNode || rectCount !== dict.previousRangeRectsCount || + rectCount != 1); + dict.previousRangeRectsCount = rectCount; + + let window = range.startContainer.ownerDocument.defaultView.top; + let document = window.document; + // First see if we need to and can remove the previous outline nodes. + if (rebuildOutline && outlineAnonNode) { + if (kDebug) { + outlineAnonNode.remove(); + } else { + try { + document.removeAnonymousContent(outlineAnonNode); + } catch (ex) {} + } + dict.modalHighlightOutline = null; + } + + // Abort when there's no text to highlight. + if (!textContent.length) + return; + + let outlineBox; + if (rebuildOutline) { + // Create the main (yellow) highlight outline box. + outlineBox = document.createElementNS(kNSHTML, "div"); + outlineBox.setAttribute("id", kModalOutlineId); + } + + const kModalOutlineTextId = kModalOutlineId + "-text"; + let i = 0; + for (let rect of rects) { + // if the current rect is the last rect, then text is set to the rest of + // the textContent with single spaces injected between the text. Otherwise + // text is set to the current textContent for the matching rect. + let text = (i == rectCount - 1) ? textContent.slice(i).join(" ") : textContent[i]; + + // Next up is to check of the outline box' borders will not overlap with + // rects that we drew before or will draw after this one. + // We're taking the width of the border into account, which is + // `kOutlineBoxBorderSize` pixels. + // When left and/ or right sides will overlap with the current, previous + // or next rect, make sure to make the necessary adjustments to the style. + // These adjustments will override the styles as defined in `kModalStyles.outlineNode`. + let intersectingSides = new Set(); + let previous = rects[i - 1]; + if (previous && + rect.left - previous.right <= 2 * kOutlineBoxBorderSize) { + intersectingSides.add("left"); + } + let next = rects[i + 1]; + if (next && + next.left - rect.right <= 2 * kOutlineBoxBorderSize) { + intersectingSides.add("right"); + } + let borderStyles = [...intersectingSides].map(side => [ "border-" + side, 0 ]); + if (intersectingSides.size) { + borderStyles.push([ "margin", `-${kOutlineBoxBorderSize}px 0 0 ${ + intersectingSides.has("left") ? 0 : -kOutlineBoxBorderSize}px !important`]); + borderStyles.push([ "border-radius", + (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) + "px " + + (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) + "px " + + (intersectingSides.has("right") ? 0 : kOutlineBoxBorderRadius) + "px " + + (intersectingSides.has("left") ? 0 : kOutlineBoxBorderRadius) + "px" ]); + } + + ++i; + let outlineStyle = this._getStyleString(kModalStyles.outlineNode, [ + ["top", rect.top + "px"], + ["left", rect.left + "px"], + ["height", rect.height + "px"], + ["width", rect.width + "px"] + ], borderStyles, kDebug ? kModalStyles.outlineNodeDebug : []); + fontStyle.lineHeight = rect.height + "px"; + let textStyle = this._getStyleString(kModalStyles.outlineText) + "; " + + this._getHTMLFontStyle(fontStyle); + + if (rebuildOutline) { + let textBoxParent = (rectCount == 1) ? outlineBox : + outlineBox.appendChild(document.createElementNS(kNSHTML, "div")); + textBoxParent.setAttribute("style", outlineStyle); + + let textBox = document.createElementNS(kNSHTML, "span"); + if (rectCount == 1) + textBox.setAttribute("id", kModalOutlineTextId); + textBox.setAttribute("style", textStyle); + textBox.textContent = text; + textBoxParent.appendChild(textBox); + } else { + // Set the appropriate properties on the existing nodes, which will also + // activate the transitions. + outlineAnonNode.setAttributeForElement(kModalOutlineId, "style", outlineStyle); + outlineAnonNode.setAttributeForElement(kModalOutlineTextId, "style", textStyle); + outlineAnonNode.setTextContentForElement(kModalOutlineTextId, text); + } + } + + if (rebuildOutline) { + dict.modalHighlightOutline = kDebug ? + mockAnonymousContentNode((document.body || + document.documentElement).appendChild(outlineBox)) : + document.insertAnonymousContent(outlineBox); + } + }, + + /** + * Add a range to the list of ranges to highlight on, or cut out of, the dimmed + * background. + * + * @param {nsIDOMRange} range Range object that should be inspected + * @param {nsIDOMWindow} window Window object, whose DOM tree is being traversed + */ + _modalHighlight(range, controller, window) { + if (!this._getRangeContentArray(range).length) + return; + + this._updateRangeRects(range); + + this.show(window); + // We don't repaint the mask right away, but pass it off to a render loop of + // sorts. + this._scheduleRepaintOfMask(window); + }, + + /** + * Lazily insert the nodes we need as anonymous content into the CanvasFrame + * of a window. + * + * @param {nsIDOMWindow} window Window to draw in. + */ + _maybeCreateModalHighlightNodes(window) { + window = window.top; + let dict = this.getForWindow(window); + if (dict.modalHighlightOutline) { + if (!dict.modalHighlightAllMask) { + // Make sure to at least show the dimmed background. + this._repaintHighlightAllMask(window, false); + this._scheduleRepaintOfMask(window); + } else { + this._scheduleRepaintOfMask(window, { scrollOnly: true }); + } + return; + } + + let document = window.document; + // A hidden document doesn't accept insertAnonymousContent calls yet. + if (document.hidden) { + let onVisibilityChange = () => { + document.removeEventListener("visibilitychange", onVisibilityChange); + this._maybeCreateModalHighlightNodes(window); + }; + document.addEventListener("visibilitychange", onVisibilityChange); + return; + } + + // Make sure to at least show the dimmed background. + this._repaintHighlightAllMask(window, false); + }, + + /** + * Build and draw the mask that takes care of the dimmed background that + * overlays the current page and the mask that cuts out all the rectangles of + * the ranges that were found. + * + * @param {nsIDOMWindow} window Window to draw in. + * @param {Boolean} [paintContent] + */ + _repaintHighlightAllMask(window, paintContent = true) { + window = window.top; + let dict = this.getForWindow(window); + + const kMaskId = kModalIdPrefix + "-findbar-modalHighlight-outlineMask"; + if (!dict.modalHighlightAllMask) { + let document = window.document; + let maskNode = document.createElementNS(kNSHTML, "div"); + maskNode.setAttribute("id", kMaskId); + dict.modalHighlightAllMask = kDebug ? + mockAnonymousContentNode((document.body || document.documentElement).appendChild(maskNode)) : + document.insertAnonymousContent(maskNode); + } + + // Make sure the dimmed mask node takes the full width and height that's available. + let {width, height} = dict.lastWindowDimensions = this._getWindowDimensions(window); + if (typeof dict.brightText != "boolean" || dict.updateAllRanges) + this._detectBrightText(dict); + let maskStyle = this._getStyleString(kModalStyles.maskNode, + [ ["width", width + "px"], ["height", height + "px"] ], + dict.brightText ? kModalStyles.maskNodeBrightText : [], + paintContent ? kModalStyles.maskNodeTransition : [], + kDebug ? kModalStyles.maskNodeDebug : []); + dict.modalHighlightAllMask.setAttributeForElement(kMaskId, "style", maskStyle); + + this._updateRangeOutline(dict); + + let allRects = []; + if (paintContent || dict.modalHighlightAllMask) { + this._updateDynamicRangesRects(dict); + + let DOMRect = window.DOMRect; + for (let [range, rects] of dict.modalHighlightRectsMap) { + if (dict.updateAllRanges) + rects = this._updateRangeRects(range); + + // If a geometry change was detected, we bail out right away here, because + // the current set of ranges has been invalidated. + if (dict.detectedGeometryChange) + return; + + for (let rect of rects) + allRects.push(new DOMRect(rect.x, rect.y, rect.width, rect.height)); + } + dict.updateAllRanges = false; + } + + dict.modalHighlightAllMask.setCutoutRectsForElement(kMaskId, allRects); + }, + + /** + * Safely remove the mask AnoymousContent node from the CanvasFrame. + * + * @param {nsIDOMWindow} window + */ + _removeHighlightAllMask(window) { + window = window.top; + let dict = this.getForWindow(window); + if (!dict.modalHighlightAllMask) + return; + + // If the current window isn't the one the content was inserted into, this + // will fail, but that's fine. + if (kDebug) { + dict.modalHighlightAllMask.remove(); + } else { + try { + window.document.removeAnonymousContent(dict.modalHighlightAllMask); + } catch (ex) {} + } + dict.modalHighlightAllMask = null; + }, + + /** + * Doing a full repaint each time a range is delivered by the highlight iterator + * is way too costly, thus we pipe the frequency down to every + * `kModalHighlightRepaintLoFreqMs` milliseconds. If there are dynamic ranges + * found (see `_isInDynamicContainer()` for the definition), the frequency + * will be upscaled to `kModalHighlightRepaintHiFreqMs`. + * + * @param {nsIDOMWindow} window + * @param {Object} options Dictionary of painter hints that contains the + * following properties: + * {Boolean} contentChanged Whether the documents' content changed in the + * meantime. This happens when the DOM is updated + * whilst the page is loaded. + * {Boolean} scrollOnly TRUE when the page has scrolled in the meantime, + * which means that the dynamically positioned + * elements need to be repainted. + * {Boolean} updateAllRanges Whether to recalculate the rects of all ranges + * that were found up until now. + */ + _scheduleRepaintOfMask(window, { contentChanged, scrollOnly, updateAllRanges } = + { contentChanged: false, scrollOnly: false, updateAllRanges: false }) { + if (!this._modal) + return; + + window = window.top; + let dict = this.getForWindow(window); + let hasDynamicRanges = !!dict.dynamicRangesSet.size; + let repaintDynamicRanges = ((scrollOnly || contentChanged) && hasDynamicRanges); + + // When we request to repaint unconditionally, we mean to call + // `_repaintHighlightAllMask()` right after the timeout. + if (!dict.unconditionalRepaintRequested) + dict.unconditionalRepaintRequested = !contentChanged || repaintDynamicRanges; + // Some events, like a resize, call for recalculation of all the rects of all ranges. + if (!dict.updateAllRanges) + dict.updateAllRanges = updateAllRanges; + + if (dict.modalRepaintScheduler) + return; + + dict.modalRepaintScheduler = window.setTimeout(() => { + dict.modalRepaintScheduler = null; + + let { width: previousWidth, height: previousHeight } = dict.lastWindowDimensions; + let { width, height } = dict.lastWindowDimensions = this._getWindowDimensions(window); + let pageContentChanged = dict.detectedGeometryChange || + (Math.abs(previousWidth - width) > kContentChangeThresholdPx || + Math.abs(previousHeight - height) > kContentChangeThresholdPx); + dict.detectedGeometryChange = false; + // When the page has changed significantly enough in size, we'll restart + // the iterator with the same parameters as before to find us new ranges. + if (pageContentChanged) + this.iterator.restart(this.finder); + + if (dict.unconditionalRepaintRequested || + (dict.modalHighlightRectsMap.size && pageContentChanged)) { + dict.unconditionalRepaintRequested = false; + this._repaintHighlightAllMask(window); + } + }, hasDynamicRanges ? kModalHighlightRepaintHiFreqMs : kModalHighlightRepaintLoFreqMs); + }, + + /** + * Add event listeners to the content which will cause the modal highlight + * AnonymousContent to be re-painted or hidden. + * + * @param {nsIDOMWindow} window + */ + _addModalHighlightListeners(window) { + window = window.top; + let dict = this.getForWindow(window); + if (dict.highlightListeners) + return; + + window = window.top; + dict.highlightListeners = [ + this._scheduleRepaintOfMask.bind(this, window, { contentChanged: true }), + this._scheduleRepaintOfMask.bind(this, window, { updateAllRanges: true }), + this._scheduleRepaintOfMask.bind(this, window, { scrollOnly: true }), + this.hide.bind(this, window, null), + () => dict.busySelecting = true + ]; + let target = this.iterator._getDocShell(window).chromeEventHandler; + target.addEventListener("MozAfterPaint", dict.highlightListeners[0]); + target.addEventListener("resize", dict.highlightListeners[1]); + target.addEventListener("scroll", dict.highlightListeners[2]); + target.addEventListener("click", dict.highlightListeners[3]); + target.addEventListener("selectstart", dict.highlightListeners[4]); + }, + + /** + * Remove event listeners from content. + * + * @param {nsIDOMWindow} window + */ + _removeModalHighlightListeners(window) { + window = window.top; + let dict = this.getForWindow(window); + if (!dict.highlightListeners) + return; + + let target = this.iterator._getDocShell(window).chromeEventHandler; + target.removeEventListener("MozAfterPaint", dict.highlightListeners[0]); + target.removeEventListener("resize", dict.highlightListeners[1]); + target.removeEventListener("scroll", dict.highlightListeners[2]); + target.removeEventListener("click", dict.highlightListeners[3]); + target.removeEventListener("selectstart", dict.highlightListeners[4]); + + dict.highlightListeners = null; + }, + + /** + * For a given node returns its editable parent or null if there is none. + * It's enough to check if node is a text node and its parent's parent is + * instance of nsIDOMNSEditableElement. + * + * @param node the node we want to check + * @returns the first node in the parent chain that is editable, + * null if there is no such node + */ + _getEditableNode(node) { + if (node.nodeType === node.TEXT_NODE && node.parentNode && node.parentNode.parentNode && + node.parentNode.parentNode instanceof Ci.nsIDOMNSEditableElement) { + return node.parentNode.parentNode; + } + return null; + }, + + /** + * Add ourselves as an nsIEditActionListener and nsIDocumentStateListener for + * a given editor + * + * @param editor the editor we'd like to listen to + */ + _addEditorListeners(editor) { + if (!this._editors) { + this._editors = []; + this._stateListeners = []; + } + + let existingIndex = this._editors.indexOf(editor); + if (existingIndex == -1) { + let x = this._editors.length; + this._editors[x] = editor; + this._stateListeners[x] = this._createStateListener(); + this._editors[x].addEditActionListener(this); + this._editors[x].addDocumentStateListener(this._stateListeners[x]); + } + }, + + /** + * Helper method to unhook listeners, remove cached editors + * and keep the relevant arrays in sync + * + * @param idx the index into the array of editors/state listeners + * we wish to remove + */ + _unhookListenersAtIndex(idx) { + this._editors[idx].removeEditActionListener(this); + this._editors[idx] + .removeDocumentStateListener(this._stateListeners[idx]); + this._editors.splice(idx, 1); + this._stateListeners.splice(idx, 1); + if (!this._editors.length) { + delete this._editors; + delete this._stateListeners; + } + }, + + /** + * Remove ourselves as an nsIEditActionListener and + * nsIDocumentStateListener from a given cached editor + * + * @param editor the editor we no longer wish to listen to + */ + _removeEditorListeners(editor) { + // editor is an editor that we listen to, so therefore must be + // cached. Find the index of this editor + let idx = this._editors.indexOf(editor); + if (idx == -1) { + return; + } + // Now unhook ourselves, and remove our cached copy + this._unhookListenersAtIndex(idx); + }, + + /* + * nsIEditActionListener logic follows + * + * We implement this interface to allow us to catch the case where + * the findbar found a match in a HTML or "; + yield BrowserTestUtils.withNewTab({ gBrowser, url: "data:text/html;charset=utf-8," + encodeURIComponent(URI) }, + function* (browser) { + // Hide the first textarea. + yield ContentTask.spawn(browser, null, function() { + content.document.getElementsByTagName("textarea")[0].style.display = "none"; + }); + + let finder = browser.finder; + let listener = { + onFindResult: function () { + ok(false, "callback wasn't replaced"); + } + }; + finder.addResultListener(listener); + + function waitForFind() { + return new Promise(resolve => { + listener.onFindResult = resolve; + }) + } + + // Find the first 'e' (which should be in the second textarea). + let promiseFind = waitForFind(); + finder.fastFind("e", false, false); + let findResult = yield promiseFind; + is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "find first string"); + + let firstRect = findResult.rect; + + // Find the second 'e' (in the third textarea). + promiseFind = waitForFind(); + finder.findAgain(false, false, false); + findResult = yield promiseFind; + is(findResult.result, Ci.nsITypeAheadFind.FIND_FOUND, "find second string"); + ok(!findResult.rect.equals(firstRect), "found new string"); + + // Ensure that we properly wrap to the second textarea. + promiseFind = waitForFind(); + finder.findAgain(false, false, false); + findResult = yield promiseFind; + is(findResult.result, Ci.nsITypeAheadFind.FIND_WRAPPED, "wrapped to first string"); + ok(findResult.rect.equals(firstRect), "wrapped to original string"); + + finder.removeResultListener(listener); + }); +}); diff --git a/toolkit/modules/tests/browser/browser_Geometry.js b/toolkit/modules/tests/browser/browser_Geometry.js new file mode 100644 index 000000000..aaca79a06 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_Geometry.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 tempScope = {}; +Components.utils.import("resource://gre/modules/Geometry.jsm", tempScope); +var Point = tempScope.Point; +var Rect = tempScope.Rect; + +function test() { + ok(Rect, "Rect class exists"); + for (var fname in tests) { + tests[fname](); + } +} + +var tests = { + testGetDimensions: function() { + let r = new Rect(5, 10, 100, 50); + ok(r.left == 5, "rect has correct left value"); + ok(r.top == 10, "rect has correct top value"); + ok(r.right == 105, "rect has correct right value"); + ok(r.bottom == 60, "rect has correct bottom value"); + ok(r.width == 100, "rect has correct width value"); + ok(r.height == 50, "rect has correct height value"); + ok(r.x == 5, "rect has correct x value"); + ok(r.y == 10, "rect has correct y value"); + }, + + testIsEmpty: function() { + let r = new Rect(0, 0, 0, 10); + ok(r.isEmpty(), "rect with nonpositive width is empty"); + r = new Rect(0, 0, 10, 0); + ok(r.isEmpty(), "rect with nonpositive height is empty"); + r = new Rect(0, 0, 10, 10); + ok(!r.isEmpty(), "rect with positive dimensions is not empty"); + }, + + testRestrictTo: function() { + let r1 = new Rect(10, 10, 100, 100); + let r2 = new Rect(50, 50, 100, 100); + r1.restrictTo(r2); + ok(r1.equals(new Rect(50, 50, 60, 60)), "intersection is non-empty"); + + r1 = new Rect(10, 10, 100, 100); + r2 = new Rect(120, 120, 100, 100); + r1.restrictTo(r2); + ok(r1.isEmpty(), "intersection is empty"); + + r1 = new Rect(10, 10, 100, 100); + r2 = new Rect(0, 0, 0, 0); + r1.restrictTo(r2); + ok(r1.isEmpty(), "intersection of rect and empty is empty"); + + r1 = new Rect(0, 0, 0, 0); + r2 = new Rect(0, 0, 0, 0); + r1.restrictTo(r2); + ok(r1.isEmpty(), "intersection of empty and empty is empty"); + }, + + testExpandToContain: function() { + let r1 = new Rect(10, 10, 100, 100); + let r2 = new Rect(50, 50, 100, 100); + r1.expandToContain(r2); + ok(r1.equals(new Rect(10, 10, 140, 140)), "correct expandToContain on intersecting rectangles"); + + r1 = new Rect(10, 10, 100, 100); + r2 = new Rect(120, 120, 100, 100); + r1.expandToContain(r2); + ok(r1.equals(new Rect(10, 10, 210, 210)), "correct expandToContain on non-intersecting rectangles"); + + r1 = new Rect(10, 10, 100, 100); + r2 = new Rect(0, 0, 0, 0); + r1.expandToContain(r2); + ok(r1.equals(new Rect(10, 10, 100, 100)), "expandToContain of rect and empty is rect"); + + r1 = new Rect(10, 10, 0, 0); + r2 = new Rect(0, 0, 0, 0); + r1.expandToContain(r2); + ok(r1.isEmpty(), "expandToContain of empty and empty is empty"); + }, + + testSubtract: function testSubtract() { + function equals(rects1, rects2) { + return rects1.length == rects2.length && rects1.every(function(r, i) { + return r.equals(rects2[i]); + }); + } + + let r1 = new Rect(0, 0, 100, 100); + let r2 = new Rect(500, 500, 100, 100); + ok(equals(r1.subtract(r2), [r1]), "subtract area outside of region yields same region"); + + r1 = new Rect(0, 0, 100, 100); + r2 = new Rect(-10, -10, 50, 120); + ok(equals(r1.subtract(r2), [new Rect(40, 0, 60, 100)]), "subtracting vertical bar from edge leaves one rect"); + + r1 = new Rect(0, 0, 100, 100); + r2 = new Rect(-10, -10, 120, 50); + ok(equals(r1.subtract(r2), [new Rect(0, 40, 100, 60)]), "subtracting horizontal bar from edge leaves one rect"); + + r1 = new Rect(0, 0, 100, 100); + r2 = new Rect(40, 40, 20, 20); + ok(equals(r1.subtract(r2), [ + new Rect(0, 0, 40, 100), + new Rect(40, 0, 20, 40), + new Rect(40, 60, 20, 40), + new Rect(60, 0, 40, 100)]), + "subtracting rect in middle leaves union of rects"); + }, +}; diff --git a/toolkit/modules/tests/browser/browser_InlineSpellChecker.js b/toolkit/modules/tests/browser/browser_InlineSpellChecker.js new file mode 100644 index 000000000..2bffc9722 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_InlineSpellChecker.js @@ -0,0 +1,121 @@ +function test() { + let tempScope = {}; + Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm", tempScope); + let InlineSpellChecker = tempScope.InlineSpellChecker; + + ok(InlineSpellChecker, "InlineSpellChecker class exists"); + for (var fname in tests) { + tests[fname](); + } +} + +var tests = { + // Test various possible dictionary name to ensure they display as expected. + // XXX: This only works for the 'en-US' locale, as the testing involves localized output. + testDictionaryDisplayNames: function() { + let isc = new InlineSpellChecker(); + + // Check non-well-formed language tag. + is(isc.getDictionaryDisplayName("-invalid-"), "-invalid-", "'-invalid-' should display as '-invalid-'"); + + // XXX: It isn't clear how we'd ideally want to display variant subtags. + + // Check valid language subtag. + is(isc.getDictionaryDisplayName("en"), "English", "'en' should display as 'English'"); + is(isc.getDictionaryDisplayName("en-fonipa"), "English (fonipa)", "'en-fonipa' should display as 'English (fonipa)'"); + is(isc.getDictionaryDisplayName("en-qxqaaaaz"), "English (qxqaaaaz)", "'en-qxqaaaaz' should display as 'English (qxqaaaaz)'"); + + // Check valid language subtag and valid region subtag. + is(isc.getDictionaryDisplayName("en-US"), "English (United States)", "'en-US' should display as 'English (United States)'"); + is(isc.getDictionaryDisplayName("en-US-fonipa"), "English (United States) (fonipa)", "'en-US-fonipa' should display as 'English (United States) (fonipa)'"); + is(isc.getDictionaryDisplayName("en-US-qxqaaaaz"), "English (United States) (qxqaaaaz)", "'en-US-qxqaaaaz' should display as 'English (United States) (qxqaaaaz)'"); + + // Check valid language subtag and invalid but well-formed region subtag. + is(isc.getDictionaryDisplayName("en-WO"), "English (WO)", "'en-WO' should display as 'English (WO)'"); + is(isc.getDictionaryDisplayName("en-WO-fonipa"), "English (WO) (fonipa)", "'en-WO-fonipa' should display as 'English (WO) (fonipa)'"); + is(isc.getDictionaryDisplayName("en-WO-qxqaaaaz"), "English (WO) (qxqaaaaz)", "'en-WO-qxqaaaaz' should display as 'English (WO) (qxqaaaaz)'"); + + // Check valid language subtag and valid script subtag. + todo_is(isc.getDictionaryDisplayName("en-Cyrl"), "English / Cyrillic", "'en-Cyrl' should display as 'English / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-fonipa"), "English / Cyrillic (fonipa)", "'en-Cyrl-fonipa' should display as 'English / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-qxqaaaaz"), "English / Cyrillic (qxqaaaaz)", "'en-Cyrl-qxqaaaaz' should display as 'English / Cyrillic (qxqaaaaz)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US"), "English (United States) / Cyrillic", "'en-Cyrl-US' should display as 'English (United States) / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa"), "English (United States) / Cyrillic (fonipa)", "'en-Cyrl-US-fonipa' should display as 'English (United States) / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-qxqaaaaz"), "English (United States) / Cyrillic (qxqaaaaz)", "'en-Cyrl-US-qxqaaaaz' should display as 'English (United States) / Cyrillic (qxqaaaaz)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO"), "English (WO) / Cyrillic", "'en-Cyrl-WO' should display as 'English (WO) / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO-fonipa"), "English (WO) / Cyrillic (fonipa)", "'en-Cyrl-WO-fonipa' should display as 'English (WO) / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-WO-qxqaaaaz"), "English (WO) / Cyrillic (qxqaaaaz)", "'en-Cyrl-WO-qxqaaaaz' should display as 'English (WO) / Cyrillic (qxqaaaaz)'"); + + // Check valid language subtag and invalid but well-formed script subtag. + is(isc.getDictionaryDisplayName("en-Qaaz"), "English / Qaaz", "'en-Qaaz' should display as 'English / Qaaz'"); + is(isc.getDictionaryDisplayName("en-Qaaz-fonipa"), "English / Qaaz (fonipa)", "'en-Qaaz-fonipa' should display as 'English / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("en-Qaaz-qxqaaaaz"), "English / Qaaz (qxqaaaaz)", "'en-Qaaz-qxqaaaaz' should display as 'English / Qaaz (qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("en-Qaaz-US"), "English (United States) / Qaaz", "'en-Qaaz-US' should display as 'English (United States) / Qaaz'"); + is(isc.getDictionaryDisplayName("en-Qaaz-US-fonipa"), "English (United States) / Qaaz (fonipa)", "'en-Qaaz-US-fonipa' should display as 'English (United States) / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("en-Qaaz-US-qxqaaaaz"), "English (United States) / Qaaz (qxqaaaaz)", "'en-Qaaz-US-qxqaaaaz' should display as 'English (United States) / Qaaz (qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("en-Qaaz-WO"), "English (WO) / Qaaz", "'en-Qaaz-WO' should display as 'English (WO) / Qaaz'"); + is(isc.getDictionaryDisplayName("en-Qaaz-WO-fonipa"), "English (WO) / Qaaz (fonipa)", "'en-Qaaz-WO-fonipa' should display as 'English (WO) / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("en-Qaaz-WO-qxqaaaaz"), "English (WO) / Qaaz (qxqaaaaz)", "'en-Qaaz-WO-qxqaaaaz' should display as 'English (WO) / Qaaz (qxqaaaaz)'"); + + // Check invalid but well-formed language subtag. + is(isc.getDictionaryDisplayName("qaz"), "qaz", "'qaz' should display as 'qaz'"); + is(isc.getDictionaryDisplayName("qaz-fonipa"), "qaz (fonipa)", "'qaz-fonipa' should display as 'qaz (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-qxqaaaaz"), "qaz (qxqaaaaz)", "'qaz-qxqaaaaz' should display as 'qaz (qxqaaaaz)'"); + + // Check invalid but well-formed language subtag and valid region subtag. + is(isc.getDictionaryDisplayName("qaz-US"), "qaz (United States)", "'qaz-US' should display as 'qaz (United States)'"); + is(isc.getDictionaryDisplayName("qaz-US-fonipa"), "qaz (United States) (fonipa)", "'qaz-US-fonipa' should display as 'qaz (United States) (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-US-qxqaaaaz"), "qaz (United States) (qxqaaaaz)", "'qaz-US-qxqaaaaz' should display as 'qaz (United States) (qxqaaaaz)'"); + + // Check invalid but well-formed language subtag and invalid but well-formed region subtag. + is(isc.getDictionaryDisplayName("qaz-WO"), "qaz (WO)", "'qaz-WO' should display as 'qaz (WO)'"); + is(isc.getDictionaryDisplayName("qaz-WO-fonipa"), "qaz (WO) (fonipa)", "'qaz-WO-fonipa' should display as 'qaz (WO) (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-WO-qxqaaaaz"), "qaz (WO) (qxqaaaaz)", "'qaz-WO-qxqaaaaz' should display as 'qaz (WO) (qxqaaaaz)'"); + + // Check invalid but well-formed language subtag and valid script subtag. + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl"), "qaz / Cyrillic", "'qaz-Cyrl' should display as 'qaz / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-fonipa"), "qaz / Cyrillic (fonipa)", "'qaz-Cyrl-fonipa' should display as 'qaz / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-qxqaaaaz"), "qaz / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-qxqaaaaz' should display as 'qaz / Cyrillic (qxqaaaaz)'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US"), "qaz (United States) / Cyrillic", "'qaz-Cyrl-US' should display as 'qaz (United States) / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US-fonipa"), "qaz (United States) / Cyrillic (fonipa)", "'qaz-Cyrl-US-fonipa' should display as 'qaz (United States) / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-US-qxqaaaaz"), "qaz (United States) / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-US-qxqaaaaz' should display as 'qaz (United States) / Cyrillic (qxqaaaaz)'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO"), "qaz (WO) / Cyrillic", "'qaz-Cyrl-WO' should display as 'qaz (WO) / Cyrillic'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO-fonipa"), "qaz (WO) / Cyrillic (fonipa)", "'qaz-Cyrl-WO-fonipa' should display as 'qaz (WO) / Cyrillic (fonipa)'"); + todo_is(isc.getDictionaryDisplayName("qaz-Cyrl-WO-qxqaaaaz"), "qaz (WO) / Cyrillic (qxqaaaaz)", "'qaz-Cyrl-WO-qxqaaaaz' should display as 'qaz (WO) / Cyrillic (qxqaaaaz)'"); + + // Check invalid but well-formed language subtag and invalid but well-formed script subtag. + is(isc.getDictionaryDisplayName("qaz-Qaaz"), "qaz / Qaaz", "'qaz-Qaaz' should display as 'qaz / Qaaz'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-fonipa"), "qaz / Qaaz (fonipa)", "'qaz-Qaaz-fonipa' should display as 'qaz / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-qxqaaaaz"), "qaz / Qaaz (qxqaaaaz)", "'qaz-Qaaz-qxqaaaaz' should display as 'qaz / Qaaz (qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-US"), "qaz (United States) / Qaaz", "'qaz-Qaaz-US' should display as 'qaz (United States) / Qaaz'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-US-fonipa"), "qaz (United States) / Qaaz (fonipa)", "'qaz-Qaaz-US-fonipa' should display as 'qaz (United States) / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-US-qxqaaaaz"), "qaz (United States) / Qaaz (qxqaaaaz)", "'qaz-Qaaz-US-qxqaaaaz' should display as 'qaz (United States) / Qaaz (qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO"), "qaz (WO) / Qaaz", "'qaz-Qaaz-WO' should display as 'qaz (WO) / Qaaz'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa"), "qaz (WO) / Qaaz (fonipa)", "'qaz-Qaaz-WO-fonipa' should display as 'qaz (WO) / Qaaz (fonipa)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-qxqaaaaz"), "qaz (WO) / Qaaz (qxqaaaaz)", "'qaz-Qaaz-WO-qxqaaaaz' should display as 'qaz (WO) / Qaaz (qxqaaaaz)'"); + + // Check multiple variant subtags. + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-fonxsamp"), "English (United States) / Cyrillic (fonipa / fonxsamp)", "'en-Cyrl-US-fonipa-fonxsamp' should display as 'English (United States) / Cyrillic (fonipa / fonxsamp)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-qxqaaaaz"), "English (United States) / Cyrillic (fonipa / qxqaaaaz)", "'en-Cyrl-US-fonipa-qxqaaaaz' should display as 'English (United States) / Cyrillic (fonipa / qxqaaaaz)'"); + todo_is(isc.getDictionaryDisplayName("en-Cyrl-US-fonipa-fonxsamp-qxqaaaaz"), "English (United States) / Cyrillic (fonipa / fonxsamp / qxqaaaaz)", "'en-Cyrl-US-fonipa-fonxsamp-qxqaaaaz' should display as 'English (United States) / Cyrillic (fonipa / fonxsamp / qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-fonxsamp"), "qaz (WO) / Qaaz (fonipa / fonxsamp)", "'qaz-Qaaz-WO-fonipa-fonxsamp' should display as 'qaz (WO) / Qaaz (fonipa / fonxsamp)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-qxqaaaaz"), "qaz (WO) / Qaaz (fonipa / qxqaaaaz)", "'qaz-Qaaz-WO-fonipa-qxqaaaaz' should display as 'qaz (WO) / Qaaz (fonipa / qxqaaaaz)'"); + is(isc.getDictionaryDisplayName("qaz-Qaaz-WO-fonipa-fonxsamp-qxqaaaaz"), "qaz (WO) / Qaaz (fonipa / fonxsamp / qxqaaaaz)", "'qaz-Qaaz-WO-fonipa-fonxsamp-qxqaaaaz' should display as 'qaz (WO) / Qaaz (fonipa / fonxsamp / qxqaaaaz)'"); + + // Check numeric region subtag. + todo_is(isc.getDictionaryDisplayName("es-419"), "Spanish (Latin America and the Caribbean)", "'es-419' should display as 'Spanish (Latin America and the Caribbean)'"); + + // Check that extension subtags are ignored. + todo_is(isc.getDictionaryDisplayName("en-Cyrl-t-en-latn-m0-ungegn-2007"), "English / Cyrillic", "'en-Cyrl-t-en-latn-m0-ungegn-2007' should display as 'English / Cyrillic'"); + + // Check that privateuse subtags are ignored. + is(isc.getDictionaryDisplayName("en-x-ignore"), "English", "'en-x-ignore' should display as 'English'"); + is(isc.getDictionaryDisplayName("en-x-ignore-this"), "English", "'en-x-ignore-this' should display as 'English'"); + is(isc.getDictionaryDisplayName("en-x-ignore-this-subtag"), "English", "'en-x-ignore-this-subtag' should display as 'English'"); + + // Check that both extension and privateuse subtags are ignored. + todo_is(isc.getDictionaryDisplayName("en-Cyrl-t-en-latn-m0-ungegn-2007-x-ignore-this-subtag"), "English / Cyrillic", "'en-Cyrl-t-en-latn-m0-ungegn-2007-x-ignore-this-subtag' should display as 'English / Cyrillic'"); + + // XXX: Check grandfathered tags. + }, +}; diff --git a/toolkit/modules/tests/browser/browser_PageMetadata.js b/toolkit/modules/tests/browser/browser_PageMetadata.js new file mode 100644 index 000000000..ca6e18368 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_PageMetadata.js @@ -0,0 +1,73 @@ +/** + * Tests PageMetadata.jsm, which extracts metadata and microdata from a + * document. + */ + +var {PageMetadata} = Cu.import("resource://gre/modules/PageMetadata.jsm", {}); + +var rootURL = "http://example.com/browser/toolkit/modules/tests/browser/"; + +function promiseDocument(fileName) { + let url = rootURL + fileName; + + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.onload = () => resolve(xhr.responseXML); + xhr.onerror = () => reject(new Error("Error loading document")); + xhr.open("GET", url); + xhr.responseType = "document"; + xhr.send(); + }); +} + +/** + * Load a simple document. + */ +add_task(function* simpleDoc() { + let fileName = "metadata_simple.html"; + info(`Loading a simple page, ${fileName}`); + + let doc = yield promiseDocument(fileName); + Assert.notEqual(doc, null, + "Should have a document to analyse"); + + let data = PageMetadata.getData(doc); + Assert.notEqual(data, null, + "Should have non-null result"); + Assert.equal(data.url, rootURL + fileName, + "Should have expected url property"); + Assert.equal(data.title, "Test Title", + "Should have expected title property"); + Assert.equal(data.description, "A very simple test page", + "Should have expected title property"); +}); + +add_task(function* titlesDoc() { + let fileName = "metadata_titles.html"; + info(`Loading titles page, ${fileName}`); + + let doc = yield promiseDocument(fileName); + Assert.notEqual(doc, null, + "Should have a document to analyse"); + + let data = PageMetadata.getData(doc); + Assert.notEqual(data, null, + "Should have non-null result"); + Assert.equal(data.title, "Test Titles", + "Should use the page title, not the open graph title"); +}); + +add_task(function* titlesFallbackDoc() { + let fileName = "metadata_titles_fallback.html"; + info(`Loading titles page, ${fileName}`); + + let doc = yield promiseDocument(fileName); + Assert.notEqual(doc, null, + "Should have a document to analyse"); + + let data = PageMetadata.getData(doc); + Assert.notEqual(data, null, + "Should have non-null result"); + Assert.equal(data.title, "Title", + "Should use the open graph title"); +}); diff --git a/toolkit/modules/tests/browser/browser_PromiseMessage.js b/toolkit/modules/tests/browser/browser_PromiseMessage.js new file mode 100644 index 000000000..e967ac4c9 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_PromiseMessage.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* global Cu, BrowserTestUtils, is, ok, add_task, gBrowser */ +"use strict"; +Cu.import("resource://gre/modules/PromiseMessage.jsm", this); + + +const url = "http://example.org/tests/dom/manifest/test/resource.sjs"; + +/** + * Test basic API error conditions + */ +add_task(function* () { + yield BrowserTestUtils.withNewTab({gBrowser, url}, testPromiseMessageAPI) +}); + +function* testPromiseMessageAPI(aBrowser) { + // Reusing an existing message. + const msgKey = "DOM:WebManifest:hasManifestLink"; + const mm = aBrowser.messageManager; + const id = "this should not change"; + const foo = "neitherShouldThis"; + const data = {id, foo}; + + // This just returns false, and it doesn't matter for this test. + yield PromiseMessage.send(mm, msgKey, data); + + // Check that no new props were added + const props = Object.getOwnPropertyNames(data); + ok(props.length === 2, "There should only be 2 props"); + ok(props.includes("id"), "Has the id property"); + ok(props.includes("foo"), "Has the foo property"); + + // Check that the props didn't change. + is(data.id, id, "The id prop must not change."); + is(data.foo, foo, "The foo prop must not change."); +} diff --git a/toolkit/modules/tests/browser/browser_RemotePageManager.js b/toolkit/modules/tests/browser/browser_RemotePageManager.js new file mode 100644 index 000000000..774d33034 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_RemotePageManager.js @@ -0,0 +1,400 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_URL = "http://www.example.com/browser/toolkit/modules/tests/browser/testremotepagemanager.html"; + +var { RemotePages, RemotePageManager } = Cu.import("resource://gre/modules/RemotePageManager.jsm", {}); + +function failOnMessage(message) { + ok(false, "Should not have seen message " + message.name); +} + +function waitForMessage(port, message, expectedPort = port) { + return new Promise((resolve) => { + function listener(message) { + is(message.target, expectedPort, "Message should be from the right port."); + + port.removeMessageListener(listener); + resolve(message); + } + + port.addMessageListener(message, listener); + }); +} + +function waitForPort(url, createTab = true) { + return new Promise((resolve) => { + RemotePageManager.addRemotePageListener(url, (port) => { + RemotePageManager.removeRemotePageListener(url); + + waitForMessage(port, "RemotePage:Load").then(() => resolve(port)); + }); + + if (createTab) + gBrowser.selectedTab = gBrowser.addTab(url); + }); +} + +function waitForPage(pages) { + return new Promise((resolve) => { + function listener({ target }) { + pages.removeMessageListener("RemotePage:Init", listener); + + waitForMessage(target, "RemotePage:Load").then(() => resolve(target)); + } + + pages.addMessageListener("RemotePage:Init", listener); + gBrowser.selectedTab = gBrowser.addTab(TEST_URL); + }); +} + +function swapDocShells(browser1, browser2) { + // Swap frameLoaders. + browser1.swapDocShells(browser2); + + // Swap permanentKeys. + let tmp = browser1.permanentKey; + browser1.permanentKey = browser2.permanentKey; + browser2.permanentKey = tmp; +} + +// Test that opening a page creates a port, sends the load event and then +// navigating to a new page sends the unload event. Going back should create a +// new port +add_task(function* init_navigate() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + let loaded = new Promise(resolve => { + function listener() { + gBrowser.selectedBrowser.removeEventListener("load", listener, true); + resolve(); + } + gBrowser.selectedBrowser.addEventListener("load", listener, true); + gBrowser.loadURI("about:blank"); + }); + + yield waitForMessage(port, "RemotePage:Unload"); + + // Port should be destroyed now + try { + port.addMessageListener("Foo", failOnMessage); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } + + try { + port.sendAsyncMessage("Foo"); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } + + yield loaded; + + gBrowser.goBack(); + port = yield waitForPort(TEST_URL, false); + + port.sendAsyncMessage("Ping2"); + let message = yield waitForMessage(port, "Pong2"); + port.destroy(); + + gBrowser.removeCurrentTab(); +}); + +// Test that opening a page creates a port, sends the load event and then +// closing the tab sends the unload event +add_task(function* init_close() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + let unloadPromise = waitForMessage(port, "RemotePage:Unload"); + gBrowser.removeCurrentTab(); + yield unloadPromise; + + // Port should be destroyed now + try { + port.addMessageListener("Foo", failOnMessage); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } + + try { + port.sendAsyncMessage("Foo"); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } +}); + +// Tests that we can send messages to individual pages even when more than one +// is open +add_task(function* multiple_ports() { + let port1 = yield waitForPort(TEST_URL); + is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + let port2 = yield waitForPort(TEST_URL); + is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + port2.addMessageListener("Pong", failOnMessage); + port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 }); + let message = yield waitForMessage(port1, "Pong"); + port2.removeMessageListener("Pong", failOnMessage); + is(message.data.str, "foobar", "String should pass through"); + is(message.data.counter, 1, "Counter should be incremented"); + + port1.addMessageListener("Pong", failOnMessage); + port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 }); + message = yield waitForMessage(port2, "Pong"); + port1.removeMessageListener("Pong", failOnMessage); + is(message.data.str, "foobaz", "String should pass through"); + is(message.data.counter, 6, "Counter should be incremented"); + + let unloadPromise = waitForMessage(port2, "RemotePage:Unload"); + gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser)); + yield unloadPromise; + + try { + port2.addMessageListener("Pong", failOnMessage); + ok(false, "Should not have been able to add a new message listener to a destroyed port."); + } + catch (e) { + ok(true, "Should not have been able to add a new message listener to a destroyed port."); + } + + port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 }); + message = yield waitForMessage(port1, "Pong"); + is(message.data.str, "foobar", "String should pass through"); + is(message.data.counter, 1, "Counter should be incremented"); + + unloadPromise = waitForMessage(port1, "RemotePage:Unload"); + gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser)); + yield unloadPromise; +}); + +// Tests that swapping browser docshells doesn't break the ports +add_task(function* browser_switch() { + let port1 = yield waitForPort(TEST_URL); + is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + let browser1 = gBrowser.selectedBrowser; + port1.sendAsyncMessage("SetCookie", { value: "om nom" }); + + let port2 = yield waitForPort(TEST_URL); + is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + let browser2 = gBrowser.selectedBrowser; + port2.sendAsyncMessage("SetCookie", { value: "om nom nom" }); + + port2.addMessageListener("Cookie", failOnMessage); + port1.sendAsyncMessage("GetCookie"); + let message = yield waitForMessage(port1, "Cookie"); + port2.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom", "Should have the right cookie"); + + port1.addMessageListener("Cookie", failOnMessage); + port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 }); + message = yield waitForMessage(port2, "Cookie"); + port1.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom nom", "Should have the right cookie"); + + swapDocShells(browser1, browser2); + is(port1.browser, browser2, "Should have noticed the swap"); + is(port2.browser, browser1, "Should have noticed the swap"); + + // Cookies should have stayed the same + port2.addMessageListener("Cookie", failOnMessage); + port1.sendAsyncMessage("GetCookie"); + message = yield waitForMessage(port1, "Cookie"); + port2.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom", "Should have the right cookie"); + + port1.addMessageListener("Cookie", failOnMessage); + port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 }); + message = yield waitForMessage(port2, "Cookie"); + port1.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom nom", "Should have the right cookie"); + + swapDocShells(browser1, browser2); + is(port1.browser, browser1, "Should have noticed the swap"); + is(port2.browser, browser2, "Should have noticed the swap"); + + // Cookies should have stayed the same + port2.addMessageListener("Cookie", failOnMessage); + port1.sendAsyncMessage("GetCookie"); + message = yield waitForMessage(port1, "Cookie"); + port2.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom", "Should have the right cookie"); + + port1.addMessageListener("Cookie", failOnMessage); + port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 }); + message = yield waitForMessage(port2, "Cookie"); + port1.removeMessageListener("Cookie", failOnMessage); + is(message.data.value, "om nom nom", "Should have the right cookie"); + + let unloadPromise = waitForMessage(port2, "RemotePage:Unload"); + gBrowser.removeTab(gBrowser.getTabForBrowser(browser2)); + yield unloadPromise; + + unloadPromise = waitForMessage(port1, "RemotePage:Unload"); + gBrowser.removeTab(gBrowser.getTabForBrowser(browser1)); + yield unloadPromise; +}); + +// Tests that removeMessageListener in chrome works +add_task(function* remove_chrome_listener() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + // This relies on messages sent arriving in the same order. Pong will be + // sent back before Pong2 so if removeMessageListener fails the test will fail + port.addMessageListener("Pong", failOnMessage); + port.removeMessageListener("Pong", failOnMessage); + port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 }); + port.sendAsyncMessage("Ping2"); + yield waitForMessage(port, "Pong2"); + + let unloadPromise = waitForMessage(port, "RemotePage:Unload"); + gBrowser.removeCurrentTab(); + yield unloadPromise; +}); + +// Tests that removeMessageListener in content works +add_task(function* remove_content_listener() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + // This relies on messages sent arriving in the same order. Pong3 would be + // sent back before Pong2 so if removeMessageListener fails the test will fail + port.addMessageListener("Pong3", failOnMessage); + port.sendAsyncMessage("Ping3"); + port.sendAsyncMessage("Ping2"); + yield waitForMessage(port, "Pong2"); + + let unloadPromise = waitForMessage(port, "RemotePage:Unload"); + gBrowser.removeCurrentTab(); + yield unloadPromise; +}); + +// Test RemotePages works +add_task(function* remote_pages_basic() { + let pages = new RemotePages(TEST_URL); + let port = yield waitForPage(pages); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + // Listening to global messages should work + let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port); + gBrowser.removeCurrentTab(); + yield unloadPromise; + + pages.destroy(); + + // RemotePages should be destroyed now + try { + pages.addMessageListener("Foo", failOnMessage); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } + + try { + pages.sendAsyncMessage("Foo"); + ok(false, "Should have seen exception"); + } + catch (e) { + ok(true, "Should have seen exception"); + } +}); + +// Test sending messages to all remote pages works +add_task(function* remote_pages_multiple() { + let pages = new RemotePages(TEST_URL); + let port1 = yield waitForPage(pages); + let port2 = yield waitForPage(pages); + + let pongPorts = []; + yield new Promise((resolve) => { + function listener({ name, target, data }) { + is(name, "Pong", "Should have seen the right response."); + is(data.str, "remote_pages", "String should pass through"); + is(data.counter, 43, "Counter should be incremented"); + pongPorts.push(target); + if (pongPorts.length == 2) + resolve(); + } + + pages.addMessageListener("Pong", listener); + pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 }); + }); + + // We don't make any guarantees about which order messages are sent to known + // pages so the pongs could have come back in any order. + isnot(pongPorts[0], pongPorts[1], "Should have received pongs from different ports"); + ok(pongPorts.indexOf(port1) >= 0, "Should have seen a pong from port1"); + ok(pongPorts.indexOf(port2) >= 0, "Should have seen a pong from port2"); + + // After destroy we should see no messages + pages.addMessageListener("RemotePage:Unload", failOnMessage); + pages.destroy(); + + gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser)); + gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser)); +}); + +// Test sending various types of data across the boundary +add_task(function* send_data() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + let data = { + integer: 45, + real: 45.78, + str: "foobar", + array: [1, 2, 3, 5, 27] + }; + + port.sendAsyncMessage("SendData", data); + let message = yield waitForMessage(port, "ReceivedData"); + + ok(message.data.result, message.data.status); + + gBrowser.removeCurrentTab(); +}); + +// Test sending an object of data across the boundary +add_task(function* send_data2() { + let port = yield waitForPort(TEST_URL); + is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser"); + + let data = { + integer: 45, + real: 45.78, + str: "foobar", + array: [1, 2, 3, 5, 27] + }; + + port.sendAsyncMessage("SendData2", {data}); + let message = yield waitForMessage(port, "ReceivedData2"); + + ok(message.data.result, message.data.status); + + gBrowser.removeCurrentTab(); +}); + +add_task(function* get_ports_for_browser() { + let pages = new RemotePages(TEST_URL); + let port = yield waitForPage(pages); + // waitForPage creates a new tab and selects it by default, so + // the selected tab should be the one hosting this port. + let browser = gBrowser.selectedBrowser; + let foundPorts = pages.portsForBrowser(browser); + is(foundPorts.length, 1, "There should only be one port for this simple page"); + is(foundPorts[0], port, "Should find the port"); + gBrowser.removeCurrentTab(); +}); diff --git a/toolkit/modules/tests/browser/browser_Troubleshoot.js b/toolkit/modules/tests/browser/browser_Troubleshoot.js new file mode 100644 index 000000000..34c2a2791 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_Troubleshoot.js @@ -0,0 +1,546 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Ideally this would be an xpcshell test, but Troubleshoot relies on things +// that aren't initialized outside of a XUL app environment like AddonManager +// and the "@mozilla.org/xre/app-info;1" component. + +Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Troubleshoot.jsm"); + +function test() { + waitForExplicitFinish(); + function doNextTest() { + if (!tests.length) { + finish(); + return; + } + tests.shift()(doNextTest); + } + doNextTest(); +} + +registerCleanupFunction(function () { + // Troubleshoot.jsm is imported into the global scope -- the window -- above. + // If it's not deleted, it outlives the test and is reported as a leak. + delete window.Troubleshoot; +}); + +var tests = [ + + function snapshotSchema(done) { + Troubleshoot.snapshot(function (snapshot) { + try { + validateObject(snapshot, SNAPSHOT_SCHEMA); + ok(true, "The snapshot should conform to the schema."); + } + catch (err) { + ok(false, "Schema mismatch, " + err); + } + done(); + }); + }, + + function modifiedPreferences(done) { + let prefs = [ + "javascript.troubleshoot", + "troubleshoot.foo", + "javascript.print_to_filename", + "network.proxy.troubleshoot", + ]; + prefs.forEach(function (p) { + Services.prefs.setBoolPref(p, true); + is(Services.prefs.getBoolPref(p), true, "The pref should be set: " + p); + }); + Troubleshoot.snapshot(function (snapshot) { + let p = snapshot.modifiedPreferences; + is(p["javascript.troubleshoot"], true, + "The pref should be present because it's whitelisted " + + "but not blacklisted."); + ok(!("troubleshoot.foo" in p), + "The pref should be absent because it's not in the whitelist."); + ok(!("javascript.print_to_filename" in p), + "The pref should be absent because it's blacklisted."); + ok(!("network.proxy.troubleshoot" in p), + "The pref should be absent because it's blacklisted."); + prefs.forEach(p => Services.prefs.deleteBranch(p)); + done(); + }); + }, + + function unicodePreferences(done) { + let name = "font.name.sans-serif.x-western"; + let utf8Value = "\xc4\x8capk\xc5\xafv Krasopis" + let unicodeValue = "\u010Capk\u016Fv Krasopis"; + + // set/getCharPref work with 8bit strings (utf8) + Services.prefs.setCharPref(name, utf8Value); + + Troubleshoot.snapshot(function (snapshot) { + let p = snapshot.modifiedPreferences; + is(p[name], unicodeValue, "The pref should have correct Unicode value."); + Services.prefs.deleteBranch(name); + done(); + }); + } +]; + +// This is inspired by JSON Schema, or by the example on its Wikipedia page +// anyway. +const SNAPSHOT_SCHEMA = { + type: "object", + required: true, + properties: { + application: { + required: true, + type: "object", + properties: { + name: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + buildID: { + required: true, + type: "string", + }, + userAgent: { + required: true, + type: "string", + }, + osVersion: { + required: true, + type: "string", + }, + vendor: { + type: "string", + }, + updateChannel: { + type: "string", + }, + supportURL: { + type: "string", + }, + remoteAutoStart: { + type: "boolean", + required: true, + }, + autoStartStatus: { + type: "number", + }, + numTotalWindows: { + type: "number", + }, + numRemoteWindows: { + type: "number", + }, + safeMode: { + type: "boolean", + }, + }, + }, + crashes: { + required: false, + type: "object", + properties: { + pending: { + required: true, + type: "number", + }, + submitted: { + required: true, + type: "array", + items: { + type: "object", + properties: { + id: { + required: true, + type: "string", + }, + date: { + required: true, + type: "number", + }, + pending: { + required: true, + type: "boolean", + }, + }, + }, + }, + }, + }, + extensions: { + required: true, + type: "array", + items: { + type: "object", + properties: { + name: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + id: { + required: true, + type: "string", + }, + isActive: { + required: true, + type: "boolean", + }, + }, + }, + }, + modifiedPreferences: { + required: true, + type: "object", + }, + lockedPreferences: { + required: true, + type: "object", + }, + graphics: { + required: true, + type: "object", + properties: { + numTotalWindows: { + required: true, + type: "number", + }, + numAcceleratedWindows: { + required: true, + type: "number", + }, + windowLayerManagerType: { + type: "string", + }, + windowLayerManagerRemote: { + type: "boolean", + }, + supportsHardwareH264: { + type: "string", + }, + currentAudioBackend: { + type: "string", + }, + numAcceleratedWindowsMessage: { + type: "array", + }, + adapterDescription: { + type: "string", + }, + adapterVendorID: { + type: "string", + }, + adapterDeviceID: { + type: "string", + }, + adapterSubsysID: { + type: "string", + }, + adapterRAM: { + type: "string", + }, + adapterDrivers: { + type: "string", + }, + driverVersion: { + type: "string", + }, + driverDate: { + type: "string", + }, + adapterDescription2: { + type: "string", + }, + adapterVendorID2: { + type: "string", + }, + adapterDeviceID2: { + type: "string", + }, + adapterSubsysID2: { + type: "string", + }, + adapterRAM2: { + type: "string", + }, + adapterDrivers2: { + type: "string", + }, + driverVersion2: { + type: "string", + }, + driverDate2: { + type: "string", + }, + isGPU2Active: { + type: "boolean", + }, + direct2DEnabled: { + type: "boolean", + }, + directWriteEnabled: { + type: "boolean", + }, + directWriteVersion: { + type: "string", + }, + clearTypeParameters: { + type: "string", + }, + webglRenderer: { + type: "string", + }, + webgl2Renderer: { + type: "string", + }, + info: { + type: "object", + }, + failures: { + type: "array", + items: { + type: "string", + }, + }, + indices: { + type: "array", + items: { + type: "number", + }, + }, + featureLog: { + type: "object", + }, + crashGuards: { + type: "array", + }, + direct2DEnabledMessage: { + type: "array", + }, + }, + }, + javaScript: { + required: true, + type: "object", + properties: { + incrementalGCEnabled: { + type: "boolean", + }, + }, + }, + accessibility: { + required: true, + type: "object", + properties: { + isActive: { + required: true, + type: "boolean", + }, + forceDisabled: { + type: "number", + }, + }, + }, + libraryVersions: { + required: true, + type: "object", + properties: { + NSPR: { + required: true, + type: "object", + properties: { + minVersion: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + }, + }, + NSS: { + required: true, + type: "object", + properties: { + minVersion: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + }, + }, + NSSUTIL: { + required: true, + type: "object", + properties: { + minVersion: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + }, + }, + NSSSSL: { + required: true, + type: "object", + properties: { + minVersion: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + }, + }, + NSSSMIME: { + required: true, + type: "object", + properties: { + minVersion: { + required: true, + type: "string", + }, + version: { + required: true, + type: "string", + }, + }, + }, + }, + }, + userJS: { + required: true, + type: "object", + properties: { + exists: { + required: true, + type: "boolean", + }, + }, + }, + experiments: { + type: "array", + }, + sandbox: { + required: false, + type: "object", + properties: { + hasSeccompBPF: { + required: AppConstants.platform == "linux", + type: "boolean" + }, + hasSeccompTSync: { + required: AppConstants.platform == "linux", + type: "boolean" + }, + hasUserNamespaces: { + required: AppConstants.platform == "linux", + type: "boolean" + }, + hasPrivilegedUserNamespaces: { + required: AppConstants.platform == "linux", + type: "boolean" + }, + canSandboxContent: { + required: false, + type: "boolean" + }, + canSandboxMedia: { + required: false, + type: "boolean" + }, + contentSandboxLevel: { + required: AppConstants.MOZ_CONTENT_SANDBOX, + type: "number" + }, + }, + }, + }, +}; + +/** + * Throws an Error if obj doesn't conform to schema. That way you get a nice + * error message and a stack to help you figure out what went wrong, which you + * wouldn't get if this just returned true or false instead. There's still + * room for improvement in communicating validation failures, however. + * + * @param obj The object to validate. + * @param schema The schema that obj should conform to. + */ +function validateObject(obj, schema) { + if (obj === undefined && !schema.required) + return; + if (typeof(schema.type) != "string") + throw schemaErr("'type' must be a string", schema); + if (objType(obj) != schema.type) + throw validationErr("Object is not of the expected type", obj, schema); + let validatorFnName = "validateObject_" + schema.type; + if (!(validatorFnName in this)) + throw schemaErr("Validator function not defined for type", schema); + this[validatorFnName](obj, schema); +} + +function validateObject_object(obj, schema) { + if (typeof(schema.properties) != "object") + // Don't care what obj's properties are. + return; + // First check that all the schema's properties match the object. + for (let prop in schema.properties) + validateObject(obj[prop], schema.properties[prop]); + // Now check that the object doesn't have any properties not in the schema. + for (let prop in obj) + if (!(prop in schema.properties)) + throw validationErr("Object has property "+prop+" not in schema", obj, schema); +} + +function validateObject_array(array, schema) { + if (typeof(schema.items) != "object") + // Don't care what the array's elements are. + return; + array.forEach(elt => validateObject(elt, schema.items)); +} + +function validateObject_string(str, schema) {} +function validateObject_boolean(bool, schema) {} +function validateObject_number(num, schema) {} + +function validationErr(msg, obj, schema) { + return new Error("Validation error: " + msg + + ": object=" + JSON.stringify(obj) + + ", schema=" + JSON.stringify(schema)); +} + +function schemaErr(msg, schema) { + return new Error("Schema error: " + msg + ": " + JSON.stringify(schema)); +} + +function objType(obj) { + let type = typeof(obj); + if (type != "object") + return type; + if (Array.isArray(obj)) + return "array"; + if (obj === null) + return "null"; + return type; +} diff --git a/toolkit/modules/tests/browser/browser_WebNavigation.js b/toolkit/modules/tests/browser/browser_WebNavigation.js new file mode 100644 index 000000000..e09cb1994 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_WebNavigation.js @@ -0,0 +1,140 @@ +"use strict"; + +var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components; + +var {WebNavigation} = Cu.import("resource://gre/modules/WebNavigation.jsm", {}); + +const BASE = "http://example.com/browser/toolkit/modules/tests/browser"; +const URL = BASE + "/file_WebNavigation_page1.html"; +const FRAME = BASE + "/file_WebNavigation_page2.html"; +const FRAME2 = BASE + "/file_WebNavigation_page3.html"; + +const EVENTS = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", + "onErrorOccurred", + "onReferenceFragmentUpdated", +]; + +const REQUIRED = [ + "onBeforeNavigate", + "onCommitted", + "onDOMContentLoaded", + "onCompleted", +]; + +var expectedBrowser; +var received = []; +var completedResolve; +var waitingURL, waitingEvent; +var rootWindowID; + +function gotEvent(event, details) +{ + if (!details.url.startsWith(BASE)) { + return; + } + info(`Got ${event} ${details.url} ${details.windowId} ${details.parentWindowId}`); + + is(details.browser, expectedBrowser, "correct element"); + + received.push({url: details.url, event}); + + if (typeof(rootWindowID) == "undefined") { + rootWindowID = details.windowId; + } + + if (details.url == URL) { + is(details.windowId, rootWindowID, "root window ID correct"); + } else { + is(details.parentWindowId, rootWindowID, "parent window ID correct"); + isnot(details.windowId, rootWindowID, "window ID probably okay"); + } + + isnot(details.windowId, undefined); + isnot(details.parentWindowId, undefined); + + if (details.url == waitingURL && event == waitingEvent) { + completedResolve(); + } +} + +function loadViaFrameScript(url, event, script) +{ + // Loading via a frame script ensures that the chrome process never + // "gets ahead" of frame scripts in non-e10s mode. + received = []; + waitingURL = url; + waitingEvent = event; + expectedBrowser.messageManager.loadFrameScript("data:," + script, false); + return new Promise(resolve => { completedResolve = resolve; }); +} + +add_task(function* webnav_ordering() { + let listeners = {}; + for (let event of EVENTS) { + listeners[event] = gotEvent.bind(null, event); + WebNavigation[event].addListener(listeners[event]); + } + + gBrowser.selectedTab = gBrowser.addTab(); + let browser = gBrowser.selectedBrowser; + expectedBrowser = browser; + + yield BrowserTestUtils.browserLoaded(browser); + + yield loadViaFrameScript(URL, "onCompleted", `content.location = "${URL}";`); + + function checkRequired(url) { + for (let event of REQUIRED) { + let found = false; + for (let r of received) { + if (r.url == url && r.event == event) { + found = true; + } + } + ok(found, `Received event ${event} from ${url}`); + } + } + + checkRequired(URL); + checkRequired(FRAME); + + function checkBefore(action1, action2) { + function find(action) { + for (let i = 0; i < received.length; i++) { + if (received[i].url == action.url && received[i].event == action.event) { + return i; + } + } + return -1; + } + + let index1 = find(action1); + let index2 = find(action2); + ok(index1 != -1, `Action ${JSON.stringify(action1)} happened`); + ok(index2 != -1, `Action ${JSON.stringify(action2)} happened`); + ok(index1 < index2, `Action ${JSON.stringify(action1)} happened before ${JSON.stringify(action2)}`); + } + + checkBefore({url: URL, event: "onCommitted"}, {url: FRAME, event: "onBeforeNavigate"}); + checkBefore({url: FRAME, event: "onCompleted"}, {url: URL, event: "onCompleted"}); + + yield loadViaFrameScript(FRAME2, "onCompleted", `content.frames[0].location = "${FRAME2}";`); + + checkRequired(FRAME2); + + yield loadViaFrameScript(FRAME2 + "#ref", "onReferenceFragmentUpdated", + "content.frames[0].document.getElementById('elt').click();"); + + info("Received onReferenceFragmentUpdated from FRAME2"); + + gBrowser.removeCurrentTab(); + + for (let event of EVENTS) { + WebNavigation[event].removeListener(listeners[event]); + } +}); + diff --git a/toolkit/modules/tests/browser/browser_WebRequest.js b/toolkit/modules/tests/browser/browser_WebRequest.js new file mode 100644 index 000000000..cdb28b16c --- /dev/null +++ b/toolkit/modules/tests/browser/browser_WebRequest.js @@ -0,0 +1,214 @@ +"use strict"; + +var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components; + +var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {}); + +const BASE = "http://example.com/browser/toolkit/modules/tests/browser"; +const URL = BASE + "/file_WebRequest_page1.html"; + +var expected_browser; + +function checkType(details) +{ + let expected_type = "???"; + if (details.url.indexOf("style") != -1) { + expected_type = "stylesheet"; + } else if (details.url.indexOf("image") != -1) { + expected_type = "image"; + } else if (details.url.indexOf("script") != -1) { + expected_type = "script"; + } else if (details.url.indexOf("page1") != -1) { + expected_type = "main_frame"; + } else if (/page2|_redirection\.|dummy_page/.test(details.url)) { + expected_type = "sub_frame"; + } else if (details.url.indexOf("xhr") != -1) { + expected_type = "xmlhttprequest"; + } + is(details.type, expected_type, "resource type is correct"); +} + +var windowIDs = new Map(); + +var requested = []; + +function onBeforeRequest(details) +{ + info(`onBeforeRequest ${details.url}`); + if (details.url.startsWith(BASE)) { + requested.push(details.url); + + is(details.browser, expected_browser, "correct element"); + checkType(details); + + windowIDs.set(details.url, details.windowId); + if (details.url.indexOf("page2") != -1) { + let page1id = windowIDs.get(URL); + ok(details.windowId != page1id, "sub-frame gets its own window ID"); + is(details.parentWindowId, page1id, "parent window id is correct"); + } + } + if (details.url.indexOf("_bad.") != -1) { + return {cancel: true}; + } + return undefined; +} + +var sendHeaders = []; + +function onBeforeSendHeaders(details) +{ + info(`onBeforeSendHeaders ${details.url}`); + if (details.url.startsWith(BASE)) { + sendHeaders.push(details.url); + + is(details.browser, expected_browser, "correct element"); + checkType(details); + + let id = windowIDs.get(details.url); + is(id, details.windowId, "window ID same in onBeforeSendHeaders as onBeforeRequest"); + } + if (details.url.indexOf("_redirect.") != -1) { + return {redirectUrl: details.url.replace("_redirect.", "_good.")}; + } + return undefined; +} + +var beforeRedirect = []; + +function onBeforeRedirect(details) +{ + info(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`); + checkType(details); + if (details.url.startsWith(BASE)) { + beforeRedirect.push(details.url); + + is(details.browser, expected_browser, "correct element"); + checkType(details); + + let expectedUrl = details.url.replace("_redirect.", "_good.").replace(/\w+_redirection\..*/, "dummy_page.html") + is(details.redirectUrl, expectedUrl, "Correct redirectUrl value"); + } + let id = windowIDs.get(details.url); + is(id, details.windowId, "window ID same in onBeforeRedirect as onBeforeRequest"); + // associate stored windowId with final url + windowIDs.set(details.redirectUrl, details.windowId); + return {}; +} + +var headersReceived = []; + +function onResponseStarted(details) +{ + if (details.url.startsWith(BASE)) { + headersReceived.push(details.url); + } +} + +const expected_requested = [BASE + "/file_WebRequest_page1.html", + BASE + "/file_style_good.css", + BASE + "/file_style_bad.css", + BASE + "/file_style_redirect.css", + BASE + "/file_image_good.png", + BASE + "/file_image_bad.png", + BASE + "/file_image_redirect.png", + BASE + "/file_script_good.js", + BASE + "/file_script_bad.js", + BASE + "/file_script_redirect.js", + BASE + "/file_script_xhr.js", + BASE + "/file_WebRequest_page2.html", + BASE + "/nonexistent_script_url.js", + BASE + "/WebRequest_redirection.sjs", + BASE + "/dummy_page.html", + BASE + "/xhr_resource"]; + +const expected_sendHeaders = [BASE + "/file_WebRequest_page1.html", + BASE + "/file_style_good.css", + BASE + "/file_style_redirect.css", + BASE + "/file_image_good.png", + BASE + "/file_image_redirect.png", + BASE + "/file_script_good.js", + BASE + "/file_script_redirect.js", + BASE + "/file_script_xhr.js", + BASE + "/file_WebRequest_page2.html", + BASE + "/nonexistent_script_url.js", + BASE + "/WebRequest_redirection.sjs", + BASE + "/dummy_page.html", + BASE + "/xhr_resource"]; + +const expected_beforeRedirect = expected_sendHeaders.filter(u => /_redirect\./.test(u)) + .concat(BASE + "/WebRequest_redirection.sjs"); + +const expected_headersReceived = [BASE + "/file_WebRequest_page1.html", + BASE + "/file_style_good.css", + BASE + "/file_image_good.png", + BASE + "/file_script_good.js", + BASE + "/file_script_xhr.js", + BASE + "/file_WebRequest_page2.html", + BASE + "/nonexistent_script_url.js", + BASE + "/dummy_page.html", + BASE + "/xhr_resource"]; + +function removeDupes(list) +{ + let j = 0; + for (let i = 1; i < list.length; i++) { + if (list[i] != list[j]) { + j++; + if (i != j) { + list[j] = list[i]; + } + } + } + list.length = j + 1; +} + +function compareLists(list1, list2, kind) +{ + list1.sort(); + removeDupes(list1); + list2.sort(); + removeDupes(list2); + is(String(list1), String(list2), `${kind} URLs correct`); +} + +function* test_once() +{ + WebRequest.onBeforeRequest.addListener(onBeforeRequest, null, ["blocking"]); + WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, null, ["blocking"]); + WebRequest.onBeforeRedirect.addListener(onBeforeRedirect); + WebRequest.onResponseStarted.addListener(onResponseStarted); + + yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, + function* (browser) { + expected_browser = browser; + BrowserTestUtils.loadURI(browser, URL); + yield BrowserTestUtils.browserLoaded(expected_browser); + + expected_browser = null; + + yield ContentTask.spawn(browser, null, function() { + let win = content.wrappedJSObject; + is(win.success, 2, "Good script ran"); + is(win.failure, undefined, "Failure script didn't run"); + + let style = + content.getComputedStyle(content.document.getElementById("test"), null); + is(style.getPropertyValue("color"), "rgb(255, 0, 0)", "Good CSS loaded"); + }); + }); + + compareLists(requested, expected_requested, "requested"); + compareLists(sendHeaders, expected_sendHeaders, "sendHeaders"); + compareLists(beforeRedirect, expected_beforeRedirect, "beforeRedirect"); + compareLists(headersReceived, expected_headersReceived, "headersReceived"); + + WebRequest.onBeforeRequest.removeListener(onBeforeRequest); + WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); + WebRequest.onBeforeRedirect.removeListener(onBeforeRedirect); + WebRequest.onResponseStarted.removeListener(onResponseStarted); +} + +// Run the test twice to make sure it works with caching. +add_task(test_once); +add_task(test_once); diff --git a/toolkit/modules/tests/browser/browser_WebRequest_cookies.js b/toolkit/modules/tests/browser/browser_WebRequest_cookies.js new file mode 100644 index 000000000..b8c4f24cb --- /dev/null +++ b/toolkit/modules/tests/browser/browser_WebRequest_cookies.js @@ -0,0 +1,89 @@ +"use strict"; + +var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components; + +var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {}); + +const BASE = "http://example.com/browser/toolkit/modules/tests/browser"; +const URL = BASE + "/WebRequest_dynamic.sjs"; + +var countBefore = 0; +var countAfter = 0; + +function onBeforeSendHeaders(details) +{ + if (details.url != URL) { + return undefined; + } + + countBefore++; + + info(`onBeforeSendHeaders ${details.url}`); + let found = false; + let headers = []; + for (let {name, value} of details.requestHeaders) { + info(`Saw header ${name} '${value}'`); + if (name == "Cookie") { + is(value, "foopy=1", "Cookie is correct"); + headers.push({name, value: "blinky=1"}); + found = true; + } else { + headers.push({name, value}); + } + } + ok(found, "Saw cookie header"); + + return {requestHeaders: headers}; +} + +function onResponseStarted(details) +{ + if (details.url != URL) { + return; + } + + countAfter++; + + info(`onResponseStarted ${details.url}`); + let found = false; + for (let {name, value} of details.responseHeaders) { + info(`Saw header ${name} '${value}'`); + if (name == "Set-Cookie") { + is(value, "dinky=1", "Cookie is correct"); + found = true; + } + } + ok(found, "Saw cookie header"); +} + +add_task(function* filter_urls() { + // First load the URL so that we set cookie foopy=1. + gBrowser.selectedTab = gBrowser.addTab(URL); + yield waitForLoad(); + gBrowser.removeCurrentTab(); + + // Now load with WebRequest set up. + WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, null, ["blocking"]); + WebRequest.onResponseStarted.addListener(onResponseStarted, null); + + gBrowser.selectedTab = gBrowser.addTab(URL); + + yield waitForLoad(); + + gBrowser.removeCurrentTab(); + + WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); + WebRequest.onResponseStarted.removeListener(onResponseStarted); + + is(countBefore, 1, "onBeforeSendHeaders hit once"); + is(countAfter, 1, "onResponseStarted hit once"); +}); + +function waitForLoad(browser = gBrowser.selectedBrowser) { + return new Promise(resolve => { + browser.addEventListener("load", function listener() { + browser.removeEventListener("load", listener, true); + resolve(); + }, true); + }); +} diff --git a/toolkit/modules/tests/browser/browser_WebRequest_filtering.js b/toolkit/modules/tests/browser/browser_WebRequest_filtering.js new file mode 100644 index 000000000..a456678c1 --- /dev/null +++ b/toolkit/modules/tests/browser/browser_WebRequest_filtering.js @@ -0,0 +1,118 @@ +"use strict"; + +var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components; + +var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {}); +var {MatchPattern} = Cu.import("resource://gre/modules/MatchPattern.jsm", {}); + +const BASE = "http://example.com/browser/toolkit/modules/tests/browser"; +const URL = BASE + "/file_WebRequest_page2.html"; + +var requested = []; + +function onBeforeRequest(details) +{ + info(`onBeforeRequest ${details.url}`); + if (details.url.startsWith(BASE)) { + requested.push(details.url); + } +} + +var sendHeaders = []; + +function onBeforeSendHeaders(details) +{ + info(`onBeforeSendHeaders ${details.url}`); + if (details.url.startsWith(BASE)) { + sendHeaders.push(details.url); + } +} + +var completed = []; + +function onResponseStarted(details) +{ + if (details.url.startsWith(BASE)) { + completed.push(details.url); + } +} + +const expected_urls = [BASE + "/file_style_good.css", + BASE + "/file_style_bad.css", + BASE + "/file_style_redirect.css"]; + +function removeDupes(list) +{ + let j = 0; + for (let i = 1; i < list.length; i++) { + if (list[i] != list[j]) { + j++; + if (i != j) { + list[j] = list[i]; + } + } + } + list.length = j + 1; +} + +function compareLists(list1, list2, kind) +{ + list1.sort(); + removeDupes(list1); + list2.sort(); + removeDupes(list2); + is(String(list1), String(list2), `${kind} URLs correct`); +} + +add_task(function* filter_urls() { + let filter = {urls: new MatchPattern("*://*/*_style_*")}; + + WebRequest.onBeforeRequest.addListener(onBeforeRequest, filter, ["blocking"]); + WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, filter, ["blocking"]); + WebRequest.onResponseStarted.addListener(onResponseStarted, filter); + + gBrowser.selectedTab = gBrowser.addTab(URL); + + yield waitForLoad(); + + gBrowser.removeCurrentTab(); + + compareLists(requested, expected_urls, "requested"); + compareLists(sendHeaders, expected_urls, "sendHeaders"); + compareLists(completed, expected_urls, "completed"); + + WebRequest.onBeforeRequest.removeListener(onBeforeRequest); + WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); + WebRequest.onResponseStarted.removeListener(onResponseStarted); +}); + +add_task(function* filter_types() { + let filter = {types: ["stylesheet"]}; + + WebRequest.onBeforeRequest.addListener(onBeforeRequest, filter, ["blocking"]); + WebRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, filter, ["blocking"]); + WebRequest.onResponseStarted.addListener(onResponseStarted, filter); + + gBrowser.selectedTab = gBrowser.addTab(URL); + + yield waitForLoad(); + + gBrowser.removeCurrentTab(); + + compareLists(requested, expected_urls, "requested"); + compareLists(sendHeaders, expected_urls, "sendHeaders"); + compareLists(completed, expected_urls, "completed"); + + WebRequest.onBeforeRequest.removeListener(onBeforeRequest); + WebRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders); + WebRequest.onResponseStarted.removeListener(onResponseStarted); +}); + +function waitForLoad(browser = gBrowser.selectedBrowser) { + return new Promise(resolve => { + browser.addEventListener("load", function listener() { + browser.removeEventListener("load", listener, true); + resolve(); + }, true); + }); +} diff --git a/toolkit/modules/tests/browser/dummy_page.html b/toolkit/modules/tests/browser/dummy_page.html new file mode 100644 index 000000000..c1c9a4e04 --- /dev/null +++ b/toolkit/modules/tests/browser/dummy_page.html @@ -0,0 +1,7 @@ + + + + +

Page

+ + diff --git a/toolkit/modules/tests/browser/file_FinderSample.html b/toolkit/modules/tests/browser/file_FinderSample.html new file mode 100644 index 000000000..e952d1fe9 --- /dev/null +++ b/toolkit/modules/tests/browser/file_FinderSample.html @@ -0,0 +1,824 @@ + + + + Childe Roland + + +

"Childe Roland to the Dark Tower Came"

Robert Browning
+
+
+
+
+
+
I.
+
+
+
+
+
My first thought was, he lied in every word, +
+
That hoary cripple, with malicious eye
+
Askance to watch the working of his lie
+
+
+
On mine, and mouth scarce able to afford
+
Suppression of the glee that pursed and scored +
+
Its edge, at one more victim gained thereby.
+
+
+
+


+
+
+
+
+
+
II.
+
+
+
+
+
What else should he be set for, with his staff? +
+
What, save to waylay with his lies, ensnare
+
All travellers who might find him posted there,
+
+
+
And ask the road? I guessed what skull-like laugh
+
Would break, what crutch 'gin write my epitaph +
+
For pastime in the dusty thoroughfare,
+
+
+
+


+
+
+
+
+
+
III.
+
+
+
+
+
If at his counsel I should turn aside +
+
Into that ominous tract which, all agree,
+
Hides the Dark Tower. Yet acquiescingly
+
+
+
I did turn as he pointed: neither pride
+
Nor hope rekindling at the end descried, +
+
So much as gladness that some end might be.
+
+
+
+


+
+
+
+
+
+
IV.
+
+
+
+
+
For, what with my whole world-wide wandering, +
+
What with my search drawn out thro' years, my hope
+
Dwindled into a ghost not fit to cope
+
+
+
With that obstreperous joy success would bring,
+
I hardly tried now to rebuke the spring +
+
My heart made, finding failure in its scope.
+
+
+
+


+
+
+
+
+
+
V.
+
+
+
+
+
As when a sick man very near to death +
+
Seems dead indeed, and feels begin and end
+
The tears and takes the farewell of each friend,
+
+
+
And hears one bid the other go, draw breath
+
Freelier outside ("since all is o'er," he saith, +
+
"And the blow fallen no grieving can amend;")
+
+
+
+


+
+
+
+
+
+
VI.
+
+
+
+
+
While some discuss if near the other graves +
+
Be room enough for this, and when a day
+
Suits best for carrying the corpse away,
+
+
+
With care about the banners, scarves and staves:
+
And still the man hears all, and only craves +
+
He may not shame such tender love and stay.
+
+
+
+


+
+
+
+
+
+
VII.
+
+
+
+
+
Thus, I had so long suffered in this quest, +
+
Heard failure prophesied so oft, been writ
+
So many times among "The Band" - to wit,
+
+
+
The knights who to the Dark Tower's search addressed
+
Their steps - that just to fail as they, seemed best, +
+
And all the doubt was now—should I be fit?
+
+
+
+


+
+
+
+
+
+
VIII.
+
+
+
+
+
So, quiet as despair, I turned from him, +
+
That hateful cripple, out of his highway
+
Into the path he pointed. All the day
+
+
+
Had been a dreary one at best, and dim
+
Was settling to its close, yet shot one grim +
+
Red leer to see the plain catch its estray.
+
+
+
+


+
+
+
+
+
+
IX.
+
+
+
+
+
For mark! no sooner was I fairly found +
+
Pledged to the plain, after a pace or two,
+
Than, pausing to throw backward a last view
+
+
+
O'er the safe road, 'twas gone; grey plain all round:
+
Nothing but plain to the horizon's bound. +
+
I might go on; nought else remained to do.
+
+
+
+


+
+
+
+
+
+
X.
+
+
+
+
+
So, on I went. I think I never saw +
+
Such starved ignoble nature; nothing throve:
+
For flowers - as well expect a cedar grove!
+
+
+
But cockle, spurge, according to their law
+
Might propagate their kind, with none to awe, +
+
You'd think; a burr had been a treasure trove.
+
+
+
+


+
+
+
+
+
+
XI.
+
+
+
+
+
No! penury, inertness and grimace, +
+
In some strange sort, were the land's portion. "See
+
Or shut your eyes," said Nature peevishly,
+
+
+
"It nothing skills: I cannot help my case:
+
'Tis the Last Judgment's fire must cure this place, +
+
Calcine its clods and set my prisoners free."
+
+
+
+


+
+
+
+
+
+
XII.
+
+
+
+
+
If there pushed any ragged thistle-stalk +
+
Above its mates, the head was chopped; the bents
+
Were jealous else. What made those holes and rents
+
+
+
In the dock's harsh swarth leaves, bruised as to baulk
+
All hope of greenness? 'tis a brute must walk +
+
Pashing their life out, with a brute's intents.
+
+
+
+


+
+
+
+
+
+
XIII.
+
+
+
+
+
As for the grass, it grew as scant as hair +
+
In leprosy; thin dry blades pricked the mud
+
Which underneath looked kneaded up with blood.
+
+
+
One stiff blind horse, his every bone a-stare,
+
Stood stupefied, however he came there: +
+
Thrust out past service from the devil's stud!
+
+
+
+


+
+
+
+
+
+
XIV.
+
+
+
+
+
Alive? he might be dead for aught I know, +
+
With that red gaunt and colloped neck a-strain,
+
And shut eyes underneath the rusty mane;
+
+
+
Seldom went such grotesqueness with such woe;
+
I never saw a brute I hated so; +
+
He must be wicked to deserve such pain.
+
+
+
+


+
+
+
+
+
+
XV.
+
+
+
+
+
I shut my eyes and turned them on my heart. +
+
As a man calls for wine before he fights,
+
I asked one draught of earlier, happier sights,
+
+
+
Ere fitly I could hope to play my part.
+
Think first, fight afterwards - the soldier's art: +
+
One taste of the old time sets all to rights.
+
+
+
+


+
+
+
+
+
+
XVI.
+
+
+
+
+
Not it! I fancied Cuthbert's reddening face +
+
Beneath its garniture of curly gold,
+
Dear fellow, till I almost felt him fold
+
+
+
An arm in mine to fix me to the place
+
That way he used. Alas, one night's disgrace! +
+
Out went my heart's new fire and left it cold.
+
+
+
+


+
+
+
+
+
+
XVII.
+
+
+
+
+
Giles then, the soul of honour - there he stands +
+
Frank as ten years ago when knighted first.
+
What honest men should dare (he said) he durst.
+
+
+
Good - but the scene shifts - faugh! what hangman hands
+
Pin to his breast a parchment? His own bands +
+
Read it. Poor traitor, spit upon and curst!
+
+
+
+


+
+
+
+
+
+
XVIII.
+
+
+
+
+
Better this present than a past like that; +
+
Back therefore to my darkening path again!
+
No sound, no sight as far as eye could strain.
+
+
+
Will the night send a howlet or a bat?
+
I asked: when something on the dismal flat +
+
Came to arrest my thoughts and change their train.
+
+
+
+


+
+
+
+
+
+
XIX.
+
+
+
+
+
A sudden little river crossed my path +
+
As unexpected as a serpent comes.
+
No sluggish tide congenial to the glooms;
+
+
+
This, as it frothed by, might have been a bath
+
For the fiend's glowing hoof - to see the wrath +
+
Of its black eddy bespate with flakes and spumes.
+
+
+
+


+
+
+
+
+
+
XX.
+
+
+
+
+
So petty yet so spiteful! All along +
+
Low scrubby alders kneeled down over it;
+
Drenched willows flung them headlong in a fit
+
+
+
Of mute despair, a suicidal throng:
+
The river which had done them all the wrong, +
+
Whate'er that was, rolled by, deterred no whit.
+
+
+
+


+
+
+
+
+
+
XXI.
+
+
+
+
+
Which, while I forded, - good saints, how I feared +
+
To set my foot upon a dead man's cheek,
+
Each step, or feel the spear I thrust to seek
+
+
+
For hollows, tangled in his hair or beard!
+
—It may have been a water-rat I speared, +
+
But, ugh! it sounded like a baby's shriek.
+
+
+
+


+
+
+
+
+
+
XXII.
+
+
+
+
+
Glad was I when I reached the other bank. +
+
Now for a better country. Vain presage!
+
Who were the strugglers, what war did they wage,
+
+
+
Whose savage trample thus could pad the dank
+
Soil to a plash? Toads in a poisoned tank, +
+
Or wild cats in a red-hot iron cage—
+
+
+
+


+
+
+
+
+
+
XXIII.
+
+
+
+
+
The fight must so have seemed in that fell cirque. +
+
What penned them there, with all the plain to choose?
+
No foot-print leading to that horrid mews,
+
+
+
None out of it. Mad brewage set to work
+
Their brains, no doubt, like galley-slaves the Turk +
+
Pits for his pastime, Christians against Jews.
+
+
+
+


+
+
+
+
+
+
XXIV.
+
+
+
+
+
And more than that - a furlong on - why, there! +
+
What bad use was that engine for, that wheel,
+
Or brake, not wheel - that harrow fit to reel
+
+
+
Men's bodies out like silk? with all the air
+
Of Tophet's tool, on earth left unaware, +
+
Or brought to sharpen its rusty teeth of steel.
+
+
+
+


+
+
+
+
+
+
XXV.
+
+
+
+
+
Then came a bit of stubbed ground, once a wood, +
+
Next a marsh, it would seem, and now mere earth
+
Desperate and done with; (so a fool finds mirth,
+
+
+
Makes a thing and then mars it, till his mood
+
Changes and off he goes!) within a rood— +
+
Bog, clay and rubble, sand and stark black dearth.
+
+
+
+


+
+
+
+
+
+
XXVI.
+
+
+
+
+
Now blotches rankling, coloured gay and grim, +
+
Now patches where some leanness of the soil's
+
Broke into moss or substances like boils;
+
+
+
Then came some palsied oak, a cleft in him
+
Like a distorted mouth that splits its rim +
+
Gaping at death, and dies while it recoils.
+
+
+
+


+
+
+
+
+
+
XXVII.
+
+
+
+
+
And just as far as ever from the end! +
+
Nought in the distance but the evening, nought
+
To point my footstep further! At the thought,
+
+
+
A great black bird, Apollyon's bosom-friend,
+
Sailed past, nor beat his wide wing dragon-penned +
+
That brushed my cap—perchance the guide I sought.
+
+
+
+


+
+
+
+
+
+
XXVIII.
+
+
+
+
+
For, looking up, aware I somehow grew, +
+
'Spite of the dusk, the plain had given place
+
All round to mountains - with such name to grace
+
+
+
Mere ugly heights and heaps now stolen in view.
+
How thus they had surprised me, - solve it, you! +
+
How to get from them was no clearer case.
+
+
+
+


+
+
+
+
+
+
XXIX.
+
+
+
+
+
Yet half I seemed to recognise some trick +
+
Of mischief happened to me, God knows when—
+
In a bad dream perhaps. Here ended, then,
+
+
+
Progress this way. When, in the very nick
+
Of giving up, one time more, came a click +
+
As when a trap shuts - you're inside the den!
+
+
+
+


+
+
+
+
+
+
XXX.
+
+
+
+
+
Burningly it came on me all at once, +
+
This was the place! those two hills on the right,
+
Crouched like two bulls locked horn in horn in fight;
+
+
+
While to the left, a tall scalped mountain... Dunce,
+
Dotard, a-dozing at the very nonce, +
+
After a life spent training for the sight!
+
+
+
+


+
+
+
+
+
+
XXXI.
+
+
+
+
+
What in the midst lay but the Tower itself? +
+
The round squat turret, blind as the fool's heart
+
Built of brown stone, without a counterpart
+
+
+
In the whole world. The tempest's mocking elf
+
Points to the shipman thus the unseen shelf +
+
He strikes on, only when the timbers start.
+
+
+
+


+
+
+
+
+
+
XXXII.
+
+
+
+
+
Not see? because of night perhaps? - why, day +
+
Came back again for that! before it left,
+
The dying sunset kindled through a cleft:
+
+
+
The hills, like giants at a hunting, lay
+
Chin upon hand, to see the game at bay,— +
+
"Now stab and end the creature - to the heft!"
+
+
+
+


+
+
+
+
+
+
XXXIII.
+
+
+
+
+
Not hear? when noise was everywhere! it tolled +
+
Increasing like a bell. Names in my ears
+
Of all the lost adventurers my peers,—
+
+
+
How such a one was strong, and such was bold,
+
And such was fortunate, yet each of old +
+
Lost, lost! one moment knelled the woe of years.
+
+
+
+


+
+
+
+
+
+
XXXIV.
+
+
+
+
+
There they stood, ranged along the hillsides, met +
+
To view the last of me, a living frame
+
For one more picture! in a sheet of flame
+
+
+
I saw them and I knew them all. And yet
+
Dauntless the slug-horn to my lips I set, +
+
And blew "Childe Roland to the Dark Tower came."
+
+
+
+ + diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page1.html b/toolkit/modules/tests/browser/file_WebNavigation_page1.html new file mode 100644 index 000000000..1b6869756 --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebNavigation_page1.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page2.html b/toolkit/modules/tests/browser/file_WebNavigation_page2.html new file mode 100644 index 000000000..cc1acc83d --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebNavigation_page2.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/toolkit/modules/tests/browser/file_WebNavigation_page3.html b/toolkit/modules/tests/browser/file_WebNavigation_page3.html new file mode 100644 index 000000000..a0a26a2e9 --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebNavigation_page3.html @@ -0,0 +1,9 @@ + + + + + +
click me + + + diff --git a/toolkit/modules/tests/browser/file_WebRequest_page1.html b/toolkit/modules/tests/browser/file_WebRequest_page1.html new file mode 100644 index 000000000..00a0b9b4b --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebRequest_page1.html @@ -0,0 +1,29 @@ + + + + + + + + + + + +
Sample text
+ + + + + + + + + + + + + + + + + diff --git a/toolkit/modules/tests/browser/file_WebRequest_page2.html b/toolkit/modules/tests/browser/file_WebRequest_page2.html new file mode 100644 index 000000000..b2cf48f9e --- /dev/null +++ b/toolkit/modules/tests/browser/file_WebRequest_page2.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +
Sample text
+ + + + + + + + + + + + + diff --git a/toolkit/modules/tests/browser/file_image_bad.png b/toolkit/modules/tests/browser/file_image_bad.png new file mode 100644 index 000000000..4c3be5084 Binary files /dev/null and b/toolkit/modules/tests/browser/file_image_bad.png differ diff --git a/toolkit/modules/tests/browser/file_image_good.png b/toolkit/modules/tests/browser/file_image_good.png new file mode 100644 index 000000000..769c63634 Binary files /dev/null and b/toolkit/modules/tests/browser/file_image_good.png differ diff --git a/toolkit/modules/tests/browser/file_image_redirect.png b/toolkit/modules/tests/browser/file_image_redirect.png new file mode 100644 index 000000000..4c3be5084 Binary files /dev/null and b/toolkit/modules/tests/browser/file_image_redirect.png differ diff --git a/toolkit/modules/tests/browser/file_script_bad.js b/toolkit/modules/tests/browser/file_script_bad.js new file mode 100644 index 000000000..90655f136 --- /dev/null +++ b/toolkit/modules/tests/browser/file_script_bad.js @@ -0,0 +1 @@ +window.failure = true; diff --git a/toolkit/modules/tests/browser/file_script_good.js b/toolkit/modules/tests/browser/file_script_good.js new file mode 100644 index 000000000..b128e54a1 --- /dev/null +++ b/toolkit/modules/tests/browser/file_script_good.js @@ -0,0 +1 @@ +window.success = window.success ? window.success + 1 : 1; diff --git a/toolkit/modules/tests/browser/file_script_redirect.js b/toolkit/modules/tests/browser/file_script_redirect.js new file mode 100644 index 000000000..917b5d620 --- /dev/null +++ b/toolkit/modules/tests/browser/file_script_redirect.js @@ -0,0 +1,2 @@ +window.failure = true; + diff --git a/toolkit/modules/tests/browser/file_script_xhr.js b/toolkit/modules/tests/browser/file_script_xhr.js new file mode 100644 index 000000000..bc1f65eae --- /dev/null +++ b/toolkit/modules/tests/browser/file_script_xhr.js @@ -0,0 +1,3 @@ +var request = new XMLHttpRequest(); +request.open("get", "http://example.com/browser/toolkit/modules/tests/browser/xhr_resource", false); +request.send(); diff --git a/toolkit/modules/tests/browser/file_style_bad.css b/toolkit/modules/tests/browser/file_style_bad.css new file mode 100644 index 000000000..8dbc8dc7a --- /dev/null +++ b/toolkit/modules/tests/browser/file_style_bad.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/modules/tests/browser/file_style_good.css b/toolkit/modules/tests/browser/file_style_good.css new file mode 100644 index 000000000..46f9774b5 --- /dev/null +++ b/toolkit/modules/tests/browser/file_style_good.css @@ -0,0 +1,3 @@ +#test { + color: red; +} diff --git a/toolkit/modules/tests/browser/file_style_redirect.css b/toolkit/modules/tests/browser/file_style_redirect.css new file mode 100644 index 000000000..8dbc8dc7a --- /dev/null +++ b/toolkit/modules/tests/browser/file_style_redirect.css @@ -0,0 +1,3 @@ +#test { + color: green !important; +} diff --git a/toolkit/modules/tests/browser/head.js b/toolkit/modules/tests/browser/head.js new file mode 100644 index 000000000..777e087e1 --- /dev/null +++ b/toolkit/modules/tests/browser/head.js @@ -0,0 +1,23 @@ +function removeDupes(list) +{ + let j = 0; + for (let i = 1; i < list.length; i++) { + if (list[i] != list[j]) { + j++; + if (i != j) { + list[j] = list[i]; + } + } + } + list.length = j + 1; +} + +function compareLists(list1, list2, kind) +{ + list1.sort(); + removeDupes(list1); + list2.sort(); + removeDupes(list2); + is(String(list1), String(list2), `${kind} URLs correct`); +} + diff --git a/toolkit/modules/tests/browser/metadata_simple.html b/toolkit/modules/tests/browser/metadata_simple.html new file mode 100644 index 000000000..18089e399 --- /dev/null +++ b/toolkit/modules/tests/browser/metadata_simple.html @@ -0,0 +1,10 @@ + + + + Test Title + + + + Llama. + + diff --git a/toolkit/modules/tests/browser/metadata_titles.html b/toolkit/modules/tests/browser/metadata_titles.html new file mode 100644 index 000000000..bd4201304 --- /dev/null +++ b/toolkit/modules/tests/browser/metadata_titles.html @@ -0,0 +1,11 @@ + + + + Test Titles + + + + + Llama. + + diff --git a/toolkit/modules/tests/browser/metadata_titles_fallback.html b/toolkit/modules/tests/browser/metadata_titles_fallback.html new file mode 100644 index 000000000..5b71879b2 --- /dev/null +++ b/toolkit/modules/tests/browser/metadata_titles_fallback.html @@ -0,0 +1,10 @@ + + + + + + + + Llama. + + diff --git a/toolkit/modules/tests/browser/testremotepagemanager.html b/toolkit/modules/tests/browser/testremotepagemanager.html new file mode 100644 index 000000000..4303a38f5 --- /dev/null +++ b/toolkit/modules/tests/browser/testremotepagemanager.html @@ -0,0 +1,66 @@ + + + + + + + + + diff --git a/toolkit/modules/tests/chrome/.eslintrc.js b/toolkit/modules/tests/chrome/.eslintrc.js new file mode 100644 index 000000000..2c669d844 --- /dev/null +++ b/toolkit/modules/tests/chrome/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/modules/tests/chrome/chrome.ini b/toolkit/modules/tests/chrome/chrome.ini new file mode 100644 index 000000000..a27230919 --- /dev/null +++ b/toolkit/modules/tests/chrome/chrome.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_bug544442_checkCert.xul] diff --git a/toolkit/modules/tests/chrome/test_bug544442_checkCert.xul b/toolkit/modules/tests/chrome/test_bug544442_checkCert.xul new file mode 100644 index 000000000..dd0ce8fbd --- /dev/null +++ b/toolkit/modules/tests/chrome/test_bug544442_checkCert.xul @@ -0,0 +1,155 @@ + + + + + + + + + + +

+ +

+
+
diff --git a/toolkit/modules/tests/mochitest/.eslintrc.js b/toolkit/modules/tests/mochitest/.eslintrc.js new file mode 100644 index 000000000..3c788d6d6 --- /dev/null +++ b/toolkit/modules/tests/mochitest/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/modules/tests/mochitest/mochitest.ini b/toolkit/modules/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..852d95539 --- /dev/null +++ b/toolkit/modules/tests/mochitest/mochitest.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_spatial_navigation.html] diff --git a/toolkit/modules/tests/mochitest/test_spatial_navigation.html b/toolkit/modules/tests/mochitest/test_spatial_navigation.html new file mode 100644 index 000000000..c1fbb0eec --- /dev/null +++ b/toolkit/modules/tests/mochitest/test_spatial_navigation.html @@ -0,0 +1,76 @@ + + + + + + Test for Bug 698437 + + + + + + +Mozilla Bug 698437 +

+
+
+
+
+
+
+
+ + diff --git a/toolkit/modules/tests/xpcshell/.eslintrc.js b/toolkit/modules/tests/xpcshell/.eslintrc.js new file mode 100644 index 000000000..fee088c17 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/modules/tests/xpcshell/TestIntegration.jsm b/toolkit/modules/tests/xpcshell/TestIntegration.jsm new file mode 100644 index 000000000..78a0b7267 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/TestIntegration.jsm @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Internal module used to test the generation of Integration.jsm getters. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "TestIntegration", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); + +this.TestIntegration = { + value: "value", + + get valueFromThis() { + return this.value; + }, + + get property() { + return this._property; + }, + + set property(value) { + this._property = value; + }, + + method(argument) { + this.methodArgument = argument; + return "method" + argument; + }, + + asyncMethod: Task.async(function* (argument) { + this.asyncMethodArgument = argument; + return "asyncMethod" + argument; + }), +}; diff --git a/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite b/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite new file mode 100644 index 000000000..15d309df5 Binary files /dev/null and b/toolkit/modules/tests/xpcshell/chromeappsstore.sqlite differ diff --git a/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist new file mode 100644 index 000000000..5888c9c9c Binary files /dev/null and b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListBinary.plist differ diff --git a/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist new file mode 100644 index 000000000..9b6decc1e --- /dev/null +++ b/toolkit/modules/tests/xpcshell/propertyLists/bug710259_propertyListXML.plist @@ -0,0 +1,28 @@ + + + + + Boolean + + Array + + abc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + אאא + אאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאאא + 𐀀𐀀𐀀 + 2011-12-31T11:15:23Z + MjAxMS0xMi0zMVQxMToxNTozM1o= + + Negative Number + -400 + Real Number + 2.71828183 + Big Int + 9007199254740993 + Negative Big Int + -9007199254740993 + + + + diff --git a/toolkit/modules/tests/xpcshell/test_BinarySearch.js b/toolkit/modules/tests/xpcshell/test_BinarySearch.js new file mode 100644 index 000000000..f48b0bccf --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_BinarySearch.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/BinarySearch.jsm"); + +function run_test() { + // empty array + ok([], 1, false, 0); + + // one-element array + ok([2], 2, true, 0); + ok([2], 1, false, 0); + ok([2], 3, false, 1); + + // two-element array + ok([2, 4], 2, true, 0); + ok([2, 4], 4, true, 1); + ok([2, 4], 1, false, 0); + ok([2, 4], 3, false, 1); + ok([2, 4], 5, false, 2); + + // three-element array + ok([2, 4, 6], 2, true, 0); + ok([2, 4, 6], 4, true, 1); + ok([2, 4, 6], 6, true, 2); + ok([2, 4, 6], 1, false, 0); + ok([2, 4, 6], 3, false, 1); + ok([2, 4, 6], 5, false, 2); + ok([2, 4, 6], 7, false, 3); + + // duplicates + ok([2, 2], 2, true, 0); + ok([2, 2], 1, false, 0); + ok([2, 2], 3, false, 2); + + // duplicates on the left + ok([2, 2, 4], 2, true, 1); + ok([2, 2, 4], 4, true, 2); + ok([2, 2, 4], 1, false, 0); + ok([2, 2, 4], 3, false, 2); + ok([2, 2, 4], 5, false, 3); + + // duplicates on the right + ok([2, 4, 4], 2, true, 0); + ok([2, 4, 4], 4, true, 1); + ok([2, 4, 4], 1, false, 0); + ok([2, 4, 4], 3, false, 1); + ok([2, 4, 4], 5, false, 3); + + // duplicates in the middle + ok([2, 4, 4, 6], 2, true, 0); + ok([2, 4, 4, 6], 4, true, 1); + ok([2, 4, 4, 6], 6, true, 3); + ok([2, 4, 4, 6], 1, false, 0); + ok([2, 4, 4, 6], 3, false, 1); + ok([2, 4, 4, 6], 5, false, 3); + ok([2, 4, 4, 6], 7, false, 4); + + // duplicates all around + ok([2, 2, 4, 4, 6, 6], 2, true, 0); + ok([2, 2, 4, 4, 6, 6], 4, true, 2); + ok([2, 2, 4, 4, 6, 6], 6, true, 4); + ok([2, 2, 4, 4, 6, 6], 1, false, 0); + ok([2, 2, 4, 4, 6, 6], 3, false, 2); + ok([2, 2, 4, 4, 6, 6], 5, false, 4); + ok([2, 2, 4, 4, 6, 6], 7, false, 6); +} + +function ok(array, target, expectedFound, expectedIdx) { + let [found, idx] = BinarySearch.search(cmp, array, target); + do_check_eq(found, expectedFound); + do_check_eq(idx, expectedIdx); + + idx = expectedFound ? expectedIdx : -1; + do_check_eq(BinarySearch.indexOf(cmp, array, target), idx); + do_check_eq(BinarySearch.insertionIndexOf(cmp, array, target), expectedIdx); +} + +function cmp(num1, num2) { + return num1 - num2; +} diff --git a/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js b/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js new file mode 100644 index 000000000..fa61f5a01 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_CanonicalJSON.js @@ -0,0 +1,146 @@ +const { CanonicalJSON } = Components.utils.import("resource://gre/modules/CanonicalJSON.jsm"); + +function stringRepresentation(obj) { + const clone = JSON.parse(JSON.stringify(obj)); + return JSON.stringify(clone); +} + +add_task(function* test_canonicalJSON_should_preserve_array_order() { + const input = ['one', 'two', 'three']; + // No sorting should be done on arrays. + do_check_eq(CanonicalJSON.stringify(input), '["one","two","three"]'); +}); + +add_task(function* test_canonicalJSON_orders_object_keys() { + const input = [{ + b: ['two', 'three'], + a: ['zero', 'one'] + }]; + do_check_eq( + CanonicalJSON.stringify(input), + '[{"a":["zero","one"],"b":["two","three"]}]' + ); +}); + +add_task(function* test_canonicalJSON_orders_nested_object_keys() { + const input = [{ + b: {d: 'd', c: 'c'}, + a: {b: 'b', a: 'a'} + }]; + do_check_eq( + CanonicalJSON.stringify(input), + '[{"a":{"a":"a","b":"b"},"b":{"c":"c","d":"d"}}]' + ); +}); + +add_task(function* test_canonicalJSON_escapes_unicode_values() { + do_check_eq( + CanonicalJSON.stringify([{key: '✓'}]), + '[{"key":"\\u2713"}]' + ); + // Unicode codepoints should be output in lowercase. + do_check_eq( + CanonicalJSON.stringify([{key: 'é'}]), + '[{"key":"\\u00e9"}]' + ); +}); + +add_task(function* test_canonicalJSON_escapes_unicode_object_keys() { + do_check_eq( + CanonicalJSON.stringify([{'é': 'check'}]), + '[{"\\u00e9":"check"}]' + ); +}); + + +add_task(function* test_canonicalJSON_does_not_alter_input() { + const records = [ + {'foo': 'bar', 'last_modified': '12345', 'id': '1'}, + {'bar': 'baz', 'last_modified': '45678', 'id': '2'} + ]; + const serializedJSON = JSON.stringify(records); + CanonicalJSON.stringify(records); + do_check_eq(JSON.stringify(records), serializedJSON); +}); + + +add_task(function* test_canonicalJSON_preserves_data() { + const records = [ + {'foo': 'bar', 'last_modified': '12345', 'id': '1'}, + {'bar': 'baz', 'last_modified': '45678', 'id': '2'}, + ] + const serialized = CanonicalJSON.stringify(records); + const expected = '[{"foo":"bar","id":"1","last_modified":"12345"},' + + '{"bar":"baz","id":"2","last_modified":"45678"}]'; + do_check_eq(CanonicalJSON.stringify(records), expected); +}); + +add_task(function* test_canonicalJSON_does_not_add_space_separators() { + const records = [ + {'foo': 'bar', 'last_modified': '12345', 'id': '1'}, + {'bar': 'baz', 'last_modified': '45678', 'id': '2'}, + ] + const serialized = CanonicalJSON.stringify(records); + do_check_false(serialized.includes(" ")); +}); + +add_task(function* test_canonicalJSON_serializes_empty_object() { + do_check_eq(CanonicalJSON.stringify({}), "{}"); +}); + +add_task(function* test_canonicalJSON_serializes_empty_array() { + do_check_eq(CanonicalJSON.stringify([]), "[]"); +}); + +add_task(function* test_canonicalJSON_serializes_NaN() { + do_check_eq(CanonicalJSON.stringify(NaN), "null"); +}); + +add_task(function* test_canonicalJSON_serializes_inf() { + // This isn't part of the JSON standard. + do_check_eq(CanonicalJSON.stringify(Infinity), "null"); +}); + + +add_task(function* test_canonicalJSON_serializes_empty_string() { + do_check_eq(CanonicalJSON.stringify(""), '""'); +}); + +add_task(function* test_canonicalJSON_escapes_backslashes() { + do_check_eq(CanonicalJSON.stringify("This\\and this"), '"This\\\\and this"'); +}); + +add_task(function* test_canonicalJSON_handles_signed_zeros() { + // do_check_eq doesn't support comparison of -0 and 0 properly. + do_check_true(CanonicalJSON.stringify(-0) === '-0'); + do_check_true(CanonicalJSON.stringify(0) === '0'); +}); + + +add_task(function* test_canonicalJSON_with_deeply_nested_dicts() { + const records = [{ + 'a': { + 'b': 'b', + 'a': 'a', + 'c': { + 'b': 'b', + 'a': 'a', + 'c': ['b', 'a', 'c'], + 'd': {'b': 'b', 'a': 'a'}, + 'id': '1', + 'e': 1, + 'f': [2, 3, 1], + 'g': {2: 2, 3: 3, 1: { + 'b': 'b', 'a': 'a', 'c': 'c'}}}}, + 'id': '1'}] + const expected = + '[{"a":{"a":"a","b":"b","c":{"a":"a","b":"b","c":["b","a","c"],' + + '"d":{"a":"a","b":"b"},"e":1,"f":[2,3,1],"g":{' + + '"1":{"a":"a","b":"b","c":"c"},"2":2,"3":3},"id":"1"}},"id":"1"}]'; + + do_check_eq(CanonicalJSON.stringify(records), expected); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/modules/tests/xpcshell/test_Color.js b/toolkit/modules/tests/xpcshell/test_Color.js new file mode 100644 index 000000000..9bf9bf861 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Color.js @@ -0,0 +1,53 @@ +"use strict"; + +Components.utils.import("resource://gre/modules/Color.jsm"); + +function run_test() { + testRelativeLuminance(); + testIsBright(); + testContrastRatio(); + testIsContrastRatioAcceptable(); +} + +function testRelativeLuminance() { + let c = new Color(0, 0, 0); + Assert.equal(c.relativeLuminance, 0, "Black is not illuminating"); + + c = new Color(255, 255, 255); + Assert.equal(c.relativeLuminance, 1, "White is quite the luminant one"); + + c = new Color(142, 42, 142); + Assert.equal(c.relativeLuminance, 0.25263952353998204, + "This purple is not that luminant"); +} + +function testIsBright() { + let c = new Color(0, 0, 0); + Assert.equal(c.isBright, 0, "Black is bright"); + + c = new Color(255, 255, 255); + Assert.equal(c.isBright, 1, "White is bright"); +} + +function testContrastRatio() { + let c = new Color(0, 0, 0); + let c2 = new Color(255, 255, 255); + Assert.equal(c.contrastRatio(c2), 21, "Contrast between black and white is max"); + Assert.equal(c.contrastRatio(c), 1, "Contrast between equals is min"); + + let c3 = new Color(142, 42, 142); + Assert.equal(c.contrastRatio(c3), 6.05279047079964, "Contrast between black and purple"); + Assert.equal(c2.contrastRatio(c3), 3.469474137806338, "Contrast between white and purple"); +} + +function testIsContrastRatioAcceptable() { + // Let's assert what browser.js is doing for window frames. + let c = new Color(...[55, 156, 152]); + let c2 = new Color(0, 0, 0); + Assert.equal(c.r, 55, "Reds should match"); + Assert.equal(c.g, 156, "Greens should match"); + Assert.equal(c.b, 152, "Blues should match"); + Assert.ok(c.isContrastRatioAcceptable(c2), "The blue is high contrast enough"); + c = new Color(...[35, 65, 100]); + Assert.ok(!c.isContrastRatioAcceptable(c2), "The blue is not high contrast enough"); +} diff --git a/toolkit/modules/tests/xpcshell/test_DeferredTask.js b/toolkit/modules/tests/xpcshell/test_DeferredTask.js new file mode 100644 index 000000000..441f9054c --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_DeferredTask.js @@ -0,0 +1,390 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the DeferredTask.jsm module. + */ + +// Globals + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", + "resource://gre/modules/DeferredTask.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +/** + * Due to the nature of this module, most of the tests are time-dependent. All + * the timeouts are designed to occur at multiples of this granularity value, + * in milliseconds, that should be high enough to prevent intermittent failures, + * but low enough to prevent an excessive overall test execution time. + */ +const T = 100; + +/** + * Waits for the specified timeout before resolving the returned promise. + */ +function promiseTimeout(aTimeoutMs) +{ + let deferred = Promise.defer(); + do_timeout(aTimeoutMs, deferred.resolve); + return deferred.promise; +} + +function run_test() +{ + run_next_test(); +} + +// Tests + +/** + * Creates a simple DeferredTask and executes it once. + */ +add_test(function test_arm_simple() +{ + new DeferredTask(run_next_test, 10).arm(); +}); + +/** + * Checks that the delay set for the task is respected. + */ +add_test(function test_arm_delay_respected() +{ + let executed1 = false; + let executed2 = false; + + new DeferredTask(function () { + executed1 = true; + do_check_false(executed2); + }, 1*T).arm(); + + new DeferredTask(function () { + executed2 = true; + do_check_true(executed1); + run_next_test(); + }, 2*T).arm(); +}); + +/** + * Checks that calling "arm" again does not introduce further delay. + */ +add_test(function test_arm_delay_notrestarted() +{ + let executed = false; + + // Create a task that will run later. + let deferredTask = new DeferredTask(() => { executed = true; }, 4*T); + deferredTask.arm(); + + // Before the task starts, call "arm" again. + do_timeout(2*T, () => deferredTask.arm()); + + // The "arm" call should not have introduced further delays. + do_timeout(5*T, function () { + do_check_true(executed); + run_next_test(); + }); +}); + +/** + * Checks that a task runs only once when armed multiple times synchronously. + */ +add_test(function test_arm_coalesced() +{ + let executed = false; + + let deferredTask = new DeferredTask(function () { + do_check_false(executed); + executed = true; + run_next_test(); + }, 50); + + deferredTask.arm(); + deferredTask.arm(); +}); + +/** + * Checks that a task runs only once when armed multiple times synchronously, + * even when it has been created with a delay of zero milliseconds. + */ +add_test(function test_arm_coalesced_nodelay() +{ + let executed = false; + + let deferredTask = new DeferredTask(function () { + do_check_false(executed); + executed = true; + run_next_test(); + }, 0); + + deferredTask.arm(); + deferredTask.arm(); +}); + +/** + * Checks that a task can be armed again while running. + */ +add_test(function test_arm_recursive() +{ + let executed = false; + + let deferredTask = new DeferredTask(function () { + if (!executed) { + executed = true; + deferredTask.arm(); + } else { + run_next_test(); + } + }, 50); + + deferredTask.arm(); +}); + +/** + * Checks that calling "arm" while an asynchronous task is running waits until + * the task is finished before restarting the delay. + */ +add_test(function test_arm_async() +{ + let finishedExecution = false; + let finishedExecutionAgain = false; + + // Create a task that will run later. + let deferredTask = new DeferredTask(function* () { + yield promiseTimeout(4*T); + if (!finishedExecution) { + finishedExecution = true; + } else if (!finishedExecutionAgain) { + finishedExecutionAgain = true; + } + }, 2*T); + deferredTask.arm(); + + // While the task is running, call "arm" again. This will result in a wait + // of 2*T until the task finishes, then another 2*T for the normal task delay + // specified on construction. + do_timeout(4*T, function () { + do_check_true(deferredTask.isRunning); + do_check_false(finishedExecution); + deferredTask.arm(); + }); + + // This will fail in case the task was started without waiting 2*T after it + // has finished. + do_timeout(7*T, function () { + do_check_false(deferredTask.isRunning); + do_check_true(finishedExecution); + }); + + // This is in the middle of the second execution. + do_timeout(10*T, function () { + do_check_true(deferredTask.isRunning); + do_check_false(finishedExecutionAgain); + }); + + // Wait enough time to verify that the task was executed as expected. + do_timeout(13*T, function () { + do_check_false(deferredTask.isRunning); + do_check_true(finishedExecutionAgain); + run_next_test(); + }); +}); + +/** + * Checks that an armed task can be disarmed. + */ +add_test(function test_disarm() +{ + // Create a task that will run later. + let deferredTask = new DeferredTask(function () { + do_throw("This task should not run."); + }, 2*T); + deferredTask.arm(); + + // Disable execution later, but before the task starts. + do_timeout(1*T, () => deferredTask.disarm()); + + // Wait enough time to verify that the task did not run. + do_timeout(3*T, run_next_test); +}); + +/** + * Checks that calling "disarm" allows the delay to be restarted. + */ +add_test(function test_disarm_delay_restarted() +{ + let executed = false; + + let deferredTask = new DeferredTask(() => { executed = true; }, 4*T); + deferredTask.arm(); + + do_timeout(2*T, function () { + deferredTask.disarm(); + deferredTask.arm(); + }); + + do_timeout(5*T, function () { + do_check_false(executed); + }); + + do_timeout(7*T, function () { + do_check_true(executed); + run_next_test(); + }); +}); + +/** + * Checks that calling "disarm" while an asynchronous task is running does not + * prevent the task to finish. + */ +add_test(function test_disarm_async() +{ + let finishedExecution = false; + + let deferredTask = new DeferredTask(function* () { + deferredTask.arm(); + yield promiseTimeout(2*T); + finishedExecution = true; + }, 1*T); + deferredTask.arm(); + + do_timeout(2*T, function () { + do_check_true(deferredTask.isRunning); + do_check_true(deferredTask.isArmed); + do_check_false(finishedExecution); + deferredTask.disarm(); + }); + + do_timeout(4*T, function () { + do_check_false(deferredTask.isRunning); + do_check_false(deferredTask.isArmed); + do_check_true(finishedExecution); + run_next_test(); + }); +}); + +/** + * Checks that calling "arm" immediately followed by "disarm" while an + * asynchronous task is running does not cause it to run again. + */ +add_test(function test_disarm_immediate_async() +{ + let executed = false; + + let deferredTask = new DeferredTask(function* () { + do_check_false(executed); + executed = true; + yield promiseTimeout(2*T); + }, 1*T); + deferredTask.arm(); + + do_timeout(2*T, function () { + do_check_true(deferredTask.isRunning); + do_check_false(deferredTask.isArmed); + deferredTask.arm(); + deferredTask.disarm(); + }); + + do_timeout(4*T, function () { + do_check_true(executed); + do_check_false(deferredTask.isRunning); + do_check_false(deferredTask.isArmed); + run_next_test(); + }); +}); + +/** + * Checks the isArmed and isRunning properties with a synchronous task. + */ +add_test(function test_isArmed_isRunning() +{ + let deferredTask = new DeferredTask(function () { + do_check_true(deferredTask.isRunning); + do_check_false(deferredTask.isArmed); + deferredTask.arm(); + do_check_true(deferredTask.isArmed); + deferredTask.disarm(); + do_check_false(deferredTask.isArmed); + run_next_test(); + }, 50); + + do_check_false(deferredTask.isArmed); + deferredTask.arm(); + do_check_true(deferredTask.isArmed); + do_check_false(deferredTask.isRunning); +}); + +/** + * Checks that the "finalize" method executes a synchronous task. + */ +add_test(function test_finalize() +{ + let executed = false; + let timePassed = false; + + let deferredTask = new DeferredTask(function () { + do_check_false(timePassed); + executed = true; + }, 2*T); + deferredTask.arm(); + + do_timeout(1*T, () => { timePassed = true; }); + + // This should trigger the immediate execution of the task. + deferredTask.finalize().then(function () { + do_check_true(executed); + run_next_test(); + }); +}); + +/** + * Checks that the "finalize" method executes the task again from start to + * finish in case it is already running. + */ +add_test(function test_finalize_executes_entirely() +{ + let executed = false; + let executedAgain = false; + let timePassed = false; + + let deferredTask = new DeferredTask(function* () { + // The first time, we arm the timer again and set up the finalization. + if (!executed) { + deferredTask.arm(); + do_check_true(deferredTask.isArmed); + do_check_true(deferredTask.isRunning); + + deferredTask.finalize().then(function () { + // When we reach this point, the task must be finished. + do_check_true(executedAgain); + do_check_false(timePassed); + do_check_false(deferredTask.isArmed); + do_check_false(deferredTask.isRunning); + run_next_test(); + }); + + // The second execution triggered by the finalization waits 1*T for the + // current task to finish (see the timeout below), but then it must not + // wait for the 2*T specified on construction as normal task delay. The + // second execution will finish after the timeout below has passed again, + // for a total of 2*T of wait time. + do_timeout(3*T, () => { timePassed = true; }); + } + + yield promiseTimeout(1*T); + + // Just before finishing, indicate if we completed the second execution. + if (executed) { + do_check_true(deferredTask.isRunning); + executedAgain = true; + } else { + executed = true; + } + }, 2*T); + + deferredTask.arm(); +}); diff --git a/toolkit/modules/tests/xpcshell/test_FileUtils.js b/toolkit/modules/tests/xpcshell/test_FileUtils.js new file mode 100644 index 000000000..86ac74389 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_FileUtils.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/FileUtils.jsm"); + +function do_check_throws(f, result, stack) { + if (!stack) + stack = Components.stack.caller; + + try { + f(); + } catch (exc) { + if (exc.result == result) + return; + do_throw("expected result " + result + ", caught " + exc, stack); + } + do_throw("expected result " + result + ", none thrown", stack); +} + +const gProfD = do_get_profile(); + +add_test(function test_getFile() { + let file = FileUtils.getFile("ProfD", ["foobar"]); + do_check_true(file instanceof Components.interfaces.nsIFile); + do_check_false(file.exists()); + + let other = gProfD.clone(); + other.append("foobar"); + do_check_true(file.equals(other)); + + run_next_test(); +}); + +add_test(function test_getFile_nonexistentDir() { + do_check_throws(function () { + let file = FileUtils.getFile("NonexistentD", ["foobar"]); + }, Components.results.NS_ERROR_FAILURE); + + run_next_test(); +}); + +add_test(function test_getFile_createDirs() { + let file = FileUtils.getFile("ProfD", ["a", "b", "foobar"]); + do_check_true(file instanceof Components.interfaces.nsIFile); + do_check_false(file.exists()); + + let other = gProfD.clone(); + other.append("a"); + do_check_true(other.isDirectory()); + other.append("b"); + do_check_true(other.isDirectory()); + other.append("foobar"); + do_check_true(file.equals(other)); + + run_next_test(); +}); + +add_test(function test_getDir() { + let dir = FileUtils.getDir("ProfD", ["foodir"]); + do_check_true(dir instanceof Components.interfaces.nsIFile); + do_check_false(dir.exists()); + + let other = gProfD.clone(); + other.append("foodir"); + do_check_true(dir.equals(other)); + + run_next_test(); +}); + +add_test(function test_getDir_nonexistentDir() { + do_check_throws(function () { + let file = FileUtils.getDir("NonexistentD", ["foodir"]); + }, Components.results.NS_ERROR_FAILURE); + + run_next_test(); +}); + +add_test(function test_getDir_shouldCreate() { + let dir = FileUtils.getDir("ProfD", ["c", "d", "foodir"], true); + do_check_true(dir instanceof Components.interfaces.nsIFile); + do_check_true(dir.exists()); + + let other = gProfD.clone(); + other.append("c"); + do_check_true(other.isDirectory()); + other.append("d"); + do_check_true(other.isDirectory()); + other.append("foodir"); + do_check_true(dir.equals(other)); + + run_next_test(); +}); + +var openFileOutputStream_defaultFlags = function (aKind, aFileName) { + let file = FileUtils.getFile("ProfD", [aFileName]); + let fos; + do_check_true(aKind == "atomic" || aKind == "safe" || aKind == ""); + if (aKind == "atomic") { + fos = FileUtils.openAtomicFileOutputStream(file); + } else if (aKind == "safe") { + fos = FileUtils.openSafeFileOutputStream(file); + } else { + fos = FileUtils.openFileOutputStream(file); + } + do_check_true(fos instanceof Components.interfaces.nsIFileOutputStream); + if (aKind == "atomic" || aKind == "safe") { + do_check_true(fos instanceof Components.interfaces.nsISafeOutputStream); + } + + // FileUtils.openFileOutputStream or FileUtils.openAtomicFileOutputStream() + // or FileUtils.openSafeFileOutputStream() opens the stream with DEFER_OPEN + // which means the file will not be open until we write to it. + do_check_false(file.exists()); + + let data = "test_default_flags"; + fos.write(data, data.length); + do_check_true(file.exists()); + + // No nsIXULRuntime in xpcshell, so use this trick to determine whether we're + // on Windows. + if ("@mozilla.org/windows-registry-key;1" in Components.classes) { + do_check_eq(file.permissions, 0o666); + } else { + do_check_eq(file.permissions, FileUtils.PERMS_FILE); + } + + run_next_test(); +}; + +var openFileOutputStream_modeFlags = function(aKind, aFileName) { + let file = FileUtils.getFile("ProfD", [aFileName]); + let fos; + do_check_true(aKind == "atomic" || aKind == "safe" || aKind == ""); + if (aKind == "atomic") { + fos = FileUtils.openAtomicFileOutputStream(file, FileUtils.MODE_WRONLY); + } else if (aKind == "safe") { + fos = FileUtils.openSafeFileOutputStream(file, FileUtils.MODE_WRONLY); + } else { + fos = FileUtils.openFileOutputStream(file, FileUtils.MODE_WRONLY); + } + let data = "test_modeFlags"; + do_check_throws(function () { + fos.write(data, data.length); + }, Components.results.NS_ERROR_FILE_NOT_FOUND); + do_check_false(file.exists()); + + run_next_test(); +}; + +var closeFileOutputStream = function(aKind, aFileName) { + let file = FileUtils.getFile("ProfD", [aFileName]); + let fos; + do_check_true(aKind == "atomic" || aKind == "safe"); + if (aKind == "atomic") { + fos = FileUtils.openAtomicFileOutputStream(file); + } else if (aKind == "safe") { + fos = FileUtils.openSafeFileOutputStream(file); + } + + // We can write data to the stream just fine while it's open. + let data = "testClose"; + fos.write(data, data.length); + + // But once we close it, we can't anymore. + if (aKind == "atomic") { + FileUtils.closeAtomicFileOutputStream(fos); + } else if (aKind == "safe") { + FileUtils.closeSafeFileOutputStream(fos); + } + do_check_throws(function () { + fos.write(data, data.length); + }, Components.results.NS_BASE_STREAM_CLOSED); + run_next_test(); +}; + +add_test(function test_openFileOutputStream_defaultFlags() { + openFileOutputStream_defaultFlags("", "george"); +}); + +// openFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE +// as the default mode flags, but we can pass in our own if we want to. +add_test(function test_openFileOutputStream_modeFlags() { + openFileOutputStream_modeFlags("", "ringo"); +}); + +add_test(function test_openAtomicFileOutputStream_defaultFlags() { + openFileOutputStream_defaultFlags("atomic", "peiyong"); +}); + +// openAtomicFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE +// as the default mode flags, but we can pass in our own if we want to. +add_test(function test_openAtomicFileOutputStream_modeFlags() { + openFileOutputStream_modeFlags("atomic", "lin"); +}); + +add_test(function test_closeAtomicFileOutputStream() { + closeFileOutputStream("atomic", "peiyonglin"); +}); + +add_test(function test_openSafeFileOutputStream_defaultFlags() { + openFileOutputStream_defaultFlags("safe", "john"); +}); + +// openSafeFileOutputStream will uses MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE +// as the default mode flags, but we can pass in our own if we want to. +add_test(function test_openSafeFileOutputStream_modeFlags() { + openFileOutputStream_modeFlags("safe", "paul"); +}); + +add_test(function test_closeSafeFileOutputStream() { + closeFileOutputStream("safe", "georgee"); +}); + +add_test(function test_newFile() { + let testfile = FileUtils.getFile("ProfD", ["test"]); + let testpath = testfile.path; + let file = new FileUtils.File(testpath); + do_check_true(file instanceof Components.interfaces.nsILocalFile); + do_check_true(file.equals(testfile)); + do_check_eq(file.path, testpath); + run_next_test(); +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/modules/tests/xpcshell/test_FinderIterator.js b/toolkit/modules/tests/xpcshell/test_FinderIterator.js new file mode 100644 index 000000000..02c923a00 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js @@ -0,0 +1,265 @@ +const { interfaces: Ci, classes: Cc, utils: Cu } = Components; +const { FinderIterator } = Cu.import("resource://gre/modules/FinderIterator.jsm", {}); +Cu.import("resource://gre/modules/Promise.jsm"); + +var gFindResults = []; +// Stub the method that instantiates nsIFind and does all the interaction with +// the docShell to be searched through. +FinderIterator._iterateDocument = function* (word, window, finder) { + for (let range of gFindResults) + yield range; +}; + +FinderIterator._rangeStartsInLink = fakeRange => fakeRange.startsInLink; + +function FakeRange(textContent, startsInLink = false) { + this.startContainer = {}; + this.startsInLink = startsInLink; + this.toString = () => textContent; +} + +var gMockWindow = { + setTimeout(cb, delay) { + Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer) + .initWithCallback(cb, delay, Ci.nsITimer.TYPE_ONE_SHOT); + } +}; + +var gMockFinder = { + _getWindow() { return gMockWindow; } +}; + +function prepareIterator(findText, rangeCount) { + gFindResults = []; + for (let i = rangeCount; --i >= 0;) + gFindResults.push(new FakeRange(findText)); +} + +add_task(function* test_start() { + let findText = "test"; + let rangeCount = 300; + prepareIterator(findText, rangeCount); + + let count = 0; + yield FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { + onIteratorRangeFound(range) { + ++count; + Assert.equal(range.toString(), findText, "Text content should match"); + } + }, + word: findText + }); + + Assert.equal(rangeCount, count, "Amount of ranges yielded should match!"); + Assert.ok(!FinderIterator.running, "Running state should match"); + Assert.equal(FinderIterator._previousRanges.length, rangeCount, "Ranges cache should match"); + + FinderIterator.reset(); +}); + +add_task(function* test_valid_arguments() { + let findText = "foo"; + let rangeCount = 20; + prepareIterator(findText, rangeCount); + + let count = 0; + + yield FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }); + + let params = FinderIterator._previousParams; + Assert.ok(!params.linksOnly, "Default for linksOnly is false"); + Assert.ok(!params.useCache, "Default for useCache is false"); + Assert.equal(params.word, findText, "Words should match"); + + count = 0; + Assert.throws(() => FinderIterator.start({ + entireWord: false, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }), /Missing required option 'caseSensitive'/, "Should throw when missing an argument"); + FinderIterator.reset(); + + Assert.throws(() => FinderIterator.start({ + caseSensitive: false, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }), /Missing required option 'entireWord'/, "Should throw when missing an argument"); + FinderIterator.reset(); + + Assert.throws(() => FinderIterator.start({ + caseSensitive: false, + entireWord: false, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }), /Missing required option 'finder'/, "Should throw when missing an argument"); + FinderIterator.reset(); + + Assert.throws(() => FinderIterator.start({ + caseSensitive: true, + entireWord: false, + finder: gMockFinder, + word: findText + }), /Missing valid, required option 'listener'/, "Should throw when missing an argument"); + FinderIterator.reset(); + + Assert.throws(() => FinderIterator.start({ + caseSensitive: false, + entireWord: true, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + }), /Missing required option 'word'/, "Should throw when missing an argument"); + FinderIterator.reset(); + + Assert.equal(count, 0, "No ranges should've been counted"); +}); + +add_task(function* test_stop() { + let findText = "bar"; + let rangeCount = 120; + prepareIterator(findText, rangeCount); + + let count = 0; + let whenDone = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }); + + FinderIterator.stop(); + + yield whenDone; + + Assert.equal(count, 0, "Number of ranges should be 0"); + + FinderIterator.reset(); +}); + +add_task(function* test_reset() { + let findText = "tik"; + let rangeCount = 142; + prepareIterator(findText, rangeCount); + + let count = 0; + let whenDone = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }); + + Assert.ok(FinderIterator.running, "Yup, running we are"); + Assert.equal(count, 0, "Number of ranges should match 0"); + Assert.equal(FinderIterator.ranges.length, 0, "Number of ranges should match 0"); + + FinderIterator.reset(); + + Assert.ok(!FinderIterator.running, "Nope, running we are not"); + Assert.equal(FinderIterator.ranges.length, 0, "No ranges after reset"); + Assert.equal(FinderIterator._previousRanges.length, 0, "No ranges after reset"); + + yield whenDone; + + Assert.equal(count, 0, "Number of ranges should match 0"); +}); + +add_task(function* test_parallel_starts() { + let findText = "tak"; + let rangeCount = 2143; + prepareIterator(findText, rangeCount); + + // Start off the iterator. + let count = 0; + let whenDone = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }); + + yield new Promise(resolve => gMockWindow.setTimeout(resolve, 120)); + Assert.ok(FinderIterator.running, "We ought to be running here"); + + let count2 = 0; + let whenDone2 = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count2; } }, + word: findText + }); + + // Let the iterator run for a little while longer before we assert the world. + yield new Promise(resolve => gMockWindow.setTimeout(resolve, 10)); + FinderIterator.stop(); + + Assert.ok(!FinderIterator.running, "Stop means stop"); + + yield whenDone; + yield whenDone2; + + Assert.greater(count, FinderIterator.kIterationSizeMax, "At least one range should've been found"); + Assert.less(count, rangeCount, "Not all ranges should've been found"); + Assert.greater(count2, FinderIterator.kIterationSizeMax, "At least one range should've been found"); + Assert.less(count2, rangeCount, "Not all ranges should've been found"); + + Assert.equal(count2, count, "The second start was later, but should have caught up"); + + FinderIterator.reset(); +}); + +add_task(function* test_allowDistance() { + let findText = "gup"; + let rangeCount = 20; + prepareIterator(findText, rangeCount); + + // Start off the iterator. + let count = 0; + let whenDone = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count; } }, + word: findText + }); + + let count2 = 0; + let whenDone2 = FinderIterator.start({ + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count2; } }, + word: "gu" + }); + + let count3 = 0; + let whenDone3 = FinderIterator.start({ + allowDistance: 1, + caseSensitive: false, + entireWord: false, + finder: gMockFinder, + listener: { onIteratorRangeFound(range) { ++count3; } }, + word: "gu" + }); + + yield Promise.all([whenDone, whenDone2, whenDone3]); + + Assert.equal(count, rangeCount, "The first iterator invocation should yield all results"); + Assert.equal(count2, 0, "The second iterator invocation should yield _no_ results"); + Assert.equal(count3, rangeCount, "The first iterator invocation should yield all results"); + + FinderIterator.reset(); +}); diff --git a/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js new file mode 100644 index 000000000..74d5ad43d --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_GMPInstallManager.js @@ -0,0 +1,794 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components; +const URL_HOST = "http://localhost"; + +var GMPScope = Cu.import("resource://gre/modules/GMPInstallManager.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm") +Cu.import("resource://gre/modules/UpdateUtils.jsm"); + +var { computeHash } = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); +var ProductAddonCheckerScope = Cu.import("resource://gre/modules/addons/ProductAddonChecker.jsm"); + +do_get_profile(); + +function run_test() { Cu.import("resource://gre/modules/Preferences.jsm") + Preferences.set("media.gmp.log.dump", true); + Preferences.set("media.gmp.log.level", 0); + run_next_test(); +} + +/** + * Tests that the helper used for preferences works correctly + */ +add_task(function* test_prefs() { + let addon1 = "addon1", addon2 = "addon2"; + + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL, "http://not-really-used"); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, "http://not-really-used-2"); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "1", addon1); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "2", addon1); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "3", addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "4", addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, addon2); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS, true); + + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL), "http://not-really-used"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE), + "http://not-really-used-2"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon1), "1"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon1), "2"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", addon2), "3"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", addon2), "4"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, undefined, addon2), + false); + do_check_true(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_CERT_CHECKATTRS)); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, addon2); +}); + +/** + * Tests that an uninit without a check works fine + */ +add_task(function* test_checkForAddons_uninitWithoutCheck() { + let installManager = new GMPInstallManager(); + installManager.uninit(); +}); + +/** + * Tests that an uninit without an install works fine + */ +add_test(function test_checkForAddons_uninitWithoutInstall() { + overrideXHR(200, ""); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that no response returned rejects + */ +add_test(function test_checkForAddons_noResponse() { + overrideXHR(200, ""); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that no addons element returned resolves with no addons + */ +add_task(function* test_checkForAddons_noAddonsElement() { + overrideXHR(200, ""); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 0); + installManager.uninit(); +}); + +/** + * Tests that empty addons element returned resolves with no addons + */ +add_task(function* test_checkForAddons_emptyAddonsElement() { + overrideXHR(200, ""); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 0); + installManager.uninit(); +}); + +/** + * Tests that a response with the wrong root element rejects + */ +add_test(function test_checkForAddons_wrongResponseXML() { + overrideXHR(200, "3.141592653589793...."); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that a 404 error works as expected + */ +add_test(function test_checkForAddons_404Error() { + overrideXHR(404, ""); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that a xhr abort() works as expected + */ +add_test(function test_checkForAddons_abort() { + let overriddenXhr = overrideXHR(200, "", { dropRequest: true} ); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + overriddenXhr.abort(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that a defensive timeout works as expected + */ +add_test(function test_checkForAddons_timeout() { + overrideXHR(200, "", { dropRequest: true, timeout: true }); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that we throw correctly in case of ssl certification error. + */ +add_test(function test_checkForAddons_bad_ssl() { + // + // Add random stuff that cause CertUtil to require https. + // + let PREF_KEY_URL_OVERRIDE_BACKUP = + Preferences.get(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, undefined); + Preferences.reset(GMPScope.GMPPrefs.KEY_URL_OVERRIDE); + + let CERTS_BRANCH_DOT_ONE = GMPScope.GMPPrefs.KEY_CERTS_BRANCH + ".1"; + let PREF_CERTS_BRANCH_DOT_ONE_BACKUP = + Preferences.get(CERTS_BRANCH_DOT_ONE, undefined); + Services.prefs.setCharPref(CERTS_BRANCH_DOT_ONE, "funky value"); + + + overrideXHR(200, ""); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + if (PREF_KEY_URL_OVERRIDE_BACKUP) { + Preferences.set(GMPScope.GMPPrefs.KEY_URL_OVERRIDE, + PREF_KEY_URL_OVERRIDE_BACKUP); + } + if (PREF_CERTS_BRANCH_DOT_ONE_BACKUP) { + Preferences.set(CERTS_BRANCH_DOT_ONE, + PREF_CERTS_BRANCH_DOT_ONE_BACKUP); + } + run_next_test(); + }); +}); + +/** + * Tests that gettinga a funky non XML response works as expected + */ +add_test(function test_checkForAddons_notXML() { + overrideXHR(200, "3.141592653589793...."); + let installManager = new GMPInstallManager(); + let promise = installManager.checkForAddons(); + + promise.then(res => { + do_check_true(res.usedFallback); + installManager.uninit(); + run_next_test(); + }); +}); + +/** + * Tests that getting a response with a single addon works as expected + */ +add_task(function* test_checkForAddons_singleAddon() { + let responseXML = + "" + + "" + + " " + + " " + + " " + + "" + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 1); + let gmpAddon = res.gmpAddons[0]; + do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); + do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); + do_check_eq(gmpAddon.hashFunction, "sha256"); + do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); + do_check_eq(gmpAddon.version, "1.1"); + do_check_eq(gmpAddon.size, undefined); + do_check_true(gmpAddon.isValid); + do_check_false(gmpAddon.isInstalled); + installManager.uninit(); +}); + +/** + * Tests that getting a response with a single addon with the optional size + * attribute parses as expected. + */ +add_task(function* test_checkForAddons_singleAddonWithSize() { + let responseXML = + "" + + "" + + " " + + " " + + " " + + "" + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 1); + let gmpAddon = res.gmpAddons[0]; + do_check_eq(gmpAddon.id, "openh264-plugin-no-at-symbol"); + do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); + do_check_eq(gmpAddon.hashFunction, "sha256"); + do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); + do_check_eq(gmpAddon.size, 42); + do_check_eq(gmpAddon.version, "1.1"); + do_check_true(gmpAddon.isValid); + do_check_false(gmpAddon.isInstalled); + installManager.uninit(); +}); + +/** + * Tests that checking for multiple addons work correctly. + * Also tests that invalid addons work correctly. + */ +add_task(function* test_checkForAddons_multipleAddonNoUpdatesSomeInvalid() { + let responseXML = + "" + + "" + + " " + + // valid openh264 + " " + + // valid not openh264 + " " + + // noid + " " + + // no URL + " " + + // no hash function + " " + + // no hash function + " " + + // not version + " " + + " " + + "" + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 7); + let gmpAddon = res.gmpAddons[0]; + do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); + do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); + do_check_eq(gmpAddon.hashFunction, "sha256"); + do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); + do_check_eq(gmpAddon.version, "1.1"); + do_check_true(gmpAddon.isValid); + do_check_false(gmpAddon.isInstalled); + + gmpAddon = res.gmpAddons[1]; + do_check_eq(gmpAddon.id, "NOT-gmp-gmpopenh264"); + do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/NOT-gmp-gmpopenh264-1.1.zip"); + do_check_eq(gmpAddon.hashFunction, "sha512"); + do_check_eq(gmpAddon.hashValue, "141592656f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); + do_check_eq(gmpAddon.version, "9.1"); + do_check_true(gmpAddon.isValid); + do_check_false(gmpAddon.isInstalled); + + for (let i = 2; i < res.gmpAddons.length; i++) { + do_check_false(res.gmpAddons[i].isValid); + do_check_false(res.gmpAddons[i].isInstalled); + } + installManager.uninit(); +}); + +/** + * Tests that checking for addons when there are also updates available + * works as expected. + */ +add_task(function* test_checkForAddons_updatesWithAddons() { + let responseXML = + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "" + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 1); + let gmpAddon = res.gmpAddons[0]; + do_check_eq(gmpAddon.id, "gmp-gmpopenh264"); + do_check_eq(gmpAddon.URL, "http://127.0.0.1:8011/gmp-gmpopenh264-1.1.zip"); + do_check_eq(gmpAddon.hashFunction, "sha256"); + do_check_eq(gmpAddon.hashValue, "1118b90d6f645eefc2b99af17bae396636ace1e33d079c88de715177584e2aee"); + do_check_eq(gmpAddon.version, "1.1"); + do_check_true(gmpAddon.isValid); + do_check_false(gmpAddon.isInstalled); + installManager.uninit(); +}); + +/** + * Tests that installing found addons works as expected + */ +function* test_checkForAddons_installAddon(id, includeSize, wantInstallReject) { + do_print("Running installAddon for id: " + id + + ", includeSize: " + includeSize + + " and wantInstallReject: " + wantInstallReject); + let httpServer = new HttpServer(); + let dir = FileUtils.getDir("TmpD", [], true); + httpServer.registerDirectory("/", dir); + httpServer.start(-1); + let testserverPort = httpServer.identity.primaryPort; + let zipFileName = "test_" + id + "_GMP.zip"; + + let zipURL = URL_HOST + ":" + testserverPort + "/" + zipFileName; + do_print("zipURL: " + zipURL); + + let data = "e~=0.5772156649"; + let zipFile = createNewZipFile(zipFileName, data); + let hashFunc = "sha256"; + let expectedDigest = yield computeHash(hashFunc, zipFile.path); + let fileSize = zipFile.fileSize; + if (wantInstallReject) { + fileSize = 1; + } + + let responseXML = + "" + + "" + + " " + + " " + + " " + + "" + + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let res = yield installManager.checkForAddons(); + do_check_eq(res.gmpAddons.length, 1); + let gmpAddon = res.gmpAddons[0]; + do_check_false(gmpAddon.isInstalled); + + try { + let extractedPaths = yield installManager.installAddon(gmpAddon); + if (wantInstallReject) { + do_check_true(false); // installAddon() should have thrown. + } + do_check_eq(extractedPaths.length, 1); + let extractedPath = extractedPaths[0]; + + do_print("Extracted path: " + extractedPath); + + let extractedFile = Cc["@mozilla.org/file/local;1"]. + createInstance(Ci.nsIFile); + extractedFile.initWithPath(extractedPath); + do_check_true(extractedFile.exists()); + let readData = readStringFromFile(extractedFile); + do_check_eq(readData, data); + + // Make sure the prefs are set correctly + do_check_true(!!GMPScope.GMPPrefs.get( + GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, "", gmpAddon.id)); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, "", + gmpAddon.id), + "1.1"); + do_check_eq(GMPScope.GMPPrefs.get(GMPScope.GMPPrefs.KEY_PLUGIN_ABI, "", + gmpAddon.id), + UpdateUtils.ABI); + // Make sure it reports as being installed + do_check_true(gmpAddon.isInstalled); + + // Cleanup + extractedFile.parent.remove(true); + zipFile.remove(false); + httpServer.stop(function() {}); + installManager.uninit(); + } catch (ex) { + zipFile.remove(false); + if (!wantInstallReject) { + do_throw("install update should not reject " + ex.message); + } + } +} + +add_task(test_checkForAddons_installAddon.bind(null, "1", true, false)); +add_task(test_checkForAddons_installAddon.bind(null, "2", false, false)); +add_task(test_checkForAddons_installAddon.bind(null, "3", true, true)); + +/** + * Tests simpleCheckAndInstall when autoupdate is disabled for a GMP + */ +add_task(function* test_simpleCheckAndInstall_autoUpdateDisabled() { + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, false, GMPScope.OPEN_H264_ID); + let responseXML = + "" + + "" + + " " + + // valid openh264 + " " + + " " + + "" + + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let result = yield installManager.simpleCheckAndInstall(); + do_check_eq(result.status, "nothing-new-to-install"); + Preferences.reset(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK); + GMPScope.GMPPrefs.set(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, GMPScope.OPEN_H264_ID); +}); + +/** + * Tests simpleCheckAndInstall nothing to install + */ +add_task(function* test_simpleCheckAndInstall_nothingToInstall() { + let responseXML = + "" + + "" + + "" + + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let result = yield installManager.simpleCheckAndInstall(); + do_check_eq(result.status, "nothing-new-to-install"); +}); + +/** + * Tests simpleCheckAndInstall too frequent + */ +add_task(function* test_simpleCheckAndInstall_tooFrequent() { + let responseXML = + "" + + "" + + "" + + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let result = yield installManager.simpleCheckAndInstall(); + do_check_eq(result.status, "too-frequent-no-check"); +}); + +/** + * Tests that installing addons when there is no server works as expected + */ +add_test(function test_installAddon_noServer() { + let dir = FileUtils.getDir("TmpD", [], true); + let zipFileName = "test_GMP.zip"; + let zipURL = URL_HOST + ":0/" + zipFileName; + + let data = "e~=0.5772156649"; + let zipFile = createNewZipFile(zipFileName, data); + + let responseXML = + "" + + "" + + " " + + " " + + " " + + "" + + overrideXHR(200, responseXML); + let installManager = new GMPInstallManager(); + let checkPromise = installManager.checkForAddons(); + checkPromise.then(res => { + do_check_eq(res.gmpAddons.length, 1); + let gmpAddon = res.gmpAddons[0]; + + GMPInstallManager.overrideLeaveDownloadedZip = true; + let installPromise = installManager.installAddon(gmpAddon); + installPromise.then(extractedPaths => { + do_throw("No server for install should reject"); + }, err => { + do_check_true(!!err); + installManager.uninit(); + run_next_test(); + }); + }, () => { + do_throw("check should not reject for install no server"); + }); +}); + +/** + * Returns the read stream into a string + */ +function readStringFromInputStream(inputStream) { + let sis = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + sis.init(inputStream); + let text = sis.read(sis.available()); + sis.close(); + return text; +} + +/** + * Reads a string of text from a file. + * This function only works with ASCII text. + */ +function readStringFromFile(file) { + if (!file.exists()) { + do_print("readStringFromFile - file doesn't exist: " + file.path); + return null; + } + let fis = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0); + return readStringFromInputStream(fis); +} + +/** + * Bare bones XMLHttpRequest implementation for testing onprogress, onerror, + * and onload nsIDomEventListener handleEvent. + */ +function makeHandler(aVal) { + if (typeof aVal == "function") + return { handleEvent: aVal }; + return aVal; +} +/** + * Constructs a mock xhr which is used for testing different aspects + * of responses. + */ +function xhr(inputStatus, inputResponse, options) { + this.inputStatus = inputStatus; + this.inputResponse = inputResponse; + this.status = 0; + this.responseXML = null; + this._aborted = false; + this._onabort = null; + this._onprogress = null; + this._onerror = null; + this._onload = null; + this._onloadend = null; + this._ontimeout = null; + this._url = null; + this._method = null; + this._timeout = 0; + this._notified = false; + this._options = options || {}; +} +xhr.prototype = { + overrideMimeType: function(aMimetype) { }, + setRequestHeader: function(aHeader, aValue) { }, + status: null, + channel: { set notificationCallbacks(aVal) { } }, + open: function(aMethod, aUrl) { + this.channel.originalURI = Services.io.newURI(aUrl, null, null); + this._method = aMethod; this._url = aUrl; + }, + abort: function() { + this._dropRequest = true; + this._notify(["abort", "loadend"]); + }, + responseXML: null, + responseText: null, + send: function(aBody) { + do_execute_soon(function() { + try { + if (this._options.dropRequest) { + if (this._timeout > 0 && this._options.timeout) { + this._notify(["timeout", "loadend"]); + } + return; + } + this.status = this.inputStatus; + this.responseText = this.inputResponse; + try { + let parser = Cc["@mozilla.org/xmlextras/domparser;1"]. + createInstance(Ci.nsIDOMParser); + this.responseXML = parser.parseFromString(this.inputResponse, + "application/xml"); + } catch (e) { + this.responseXML = null; + } + if (this.inputStatus === 200) { + this._notify(["load", "loadend"]); + } else { + this._notify(["error", "loadend"]); + } + } catch (ex) { + do_throw(ex); + } + }.bind(this)); + }, + set onabort(aValue) { this._onabort = makeHandler(aValue); }, + get onabort() { return this._onabort; }, + set onprogress(aValue) { this._onprogress = makeHandler(aValue); }, + get onprogress() { return this._onprogress; }, + set onerror(aValue) { this._onerror = makeHandler(aValue); }, + get onerror() { return this._onerror; }, + set onload(aValue) { this._onload = makeHandler(aValue); }, + get onload() { return this._onload; }, + set onloadend(aValue) { this._onloadend = makeHandler(aValue); }, + get onloadend() { return this._onloadend; }, + set ontimeout(aValue) { this._ontimeout = makeHandler(aValue); }, + get ontimeout() { return this._ontimeout; }, + set timeout(aValue) { this._timeout = aValue; }, + _notify: function(events) { + if (this._notified) { + return; + } + this._notified = true; + for (let item of events) { + let k = "on" + item; + if (this[k]) { + do_print("Notifying " + item); + let e = { + target: this, + type: item, + }; + this[k](e); + } else { + do_print("Notifying " + item + ", but there are no listeners"); + } + } + }, + addEventListener: function(aEvent, aValue, aCapturing) { + eval("this._on" + aEvent + " = aValue"); + }, + flags: Ci.nsIClassInfo.SINGLETON, + getScriptableHelper: () => null, + getInterfaces: function(aCount) { + let interfaces = [Ci.nsISupports]; + aCount.value = interfaces.length; + return interfaces; + }, + classDescription: "XMLHttpRequest", + contractID: "@mozilla.org/xmlextras/xmlhttprequest;1", + classID: Components.ID("{c9b37f43-4278-4304-a5e0-600991ab08cb}"), + createInstance: function(aOuter, aIID) { + if (aOuter == null) + return this.QueryInterface(aIID); + throw Cr.NS_ERROR_NO_AGGREGATION; + }, + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIClassInfo) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + }, + get wrappedJSObject() { return this; } +}; + +/** + * Helper used to overrideXHR requests (no matter to what URL) with the + * specified status and response. + * @param status The status you want to get back when an XHR request is made + * @param response The response you want to get back when an XHR request is made + */ +function overrideXHR(status, response, options) { + overrideXHR.myxhr = new xhr(status, response, options); + ProductAddonCheckerScope.CreateXHR = function() { + return overrideXHR.myxhr; + }; + return overrideXHR.myxhr; +} + +/** + * Creates a new zip file containing a file with the specified data + * @param zipName The name of the zip file + * @param data The data to go inside the zip for the filename entry1.info + */ +function createNewZipFile(zipName, data) { + // Create a zip file which will be used for extracting + let stream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + stream.setData(data, data.length); + let zipWriter = Cc["@mozilla.org/zipwriter;1"]. + createInstance(Components.interfaces.nsIZipWriter); + let zipFile = FileUtils.getFile("TmpD", [zipName]); + if (zipFile.exists()) { + zipFile.remove(false); + } + // From prio.h + const PR_RDWR = 0x04; + const PR_CREATE_FILE = 0x08; + const PR_TRUNCATE = 0x20; + zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + zipWriter.addEntryStream("entry1.info", Date.now(), + Ci.nsIZipWriter.COMPRESSION_BEST, stream, false); + zipWriter.close(); + stream.close(); + do_print("zip file created on disk at: " + zipFile.path); + return zipFile; +} diff --git a/toolkit/modules/tests/xpcshell/test_Http.js b/toolkit/modules/tests/xpcshell/test_Http.js new file mode 100644 index 000000000..3dfd769b7 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Http.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Http.jsm"); +Components.utils.import("resource://testing-common/httpd.js"); + +const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); + +var server; + +const kDefaultServerPort = 9000; +const kSuccessPath = "/success"; +const kBaseUrl = "http://localhost:" + kDefaultServerPort; +const kSuccessUrl = kBaseUrl + kSuccessPath; + +const kPostPath = "/post"; +const kPostUrl = kBaseUrl + kPostPath; +const kPostDataSent = [["foo", "bar"], ["complex", "!*()@"]]; +const kPostDataReceived = "foo=bar&complex=%21%2A%28%29%40"; +const kPostMimeTypeReceived = "application/x-www-form-urlencoded; charset=utf-8"; + +const kJsonPostPath = "/json_post"; +const kJsonPostUrl = kBaseUrl + kJsonPostPath; +const kJsonPostData = JSON.stringify(kPostDataSent); +const kJsonPostMimeType = "application/json"; + +const kPutPath = "/put"; +const kPutUrl = kBaseUrl + kPutPath; +const kPutDataSent = [["P", "NP"]]; +const kPutDataReceived = "P=NP"; + +const kGetPath = "/get"; +const kGetUrl = kBaseUrl + kGetPath; + +function successResult(aRequest, aResponse) { + aResponse.setStatusLine(null, 200, "OK"); + aResponse.setHeader("Content-Type", "application/json"); + aResponse.write("Success!"); +} + +function getDataChecker(aExpectedMethod, aExpectedData, aExpectedMimeType = null) { + return function(aRequest, aResponse) { + let body = new BinaryInputStream(aRequest.bodyInputStream); + let bytes = []; + let avail; + while ((avail = body.available()) > 0) + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + + do_check_eq(aRequest.method, aExpectedMethod); + + // Checking if the Content-Type is as expected. + if (aExpectedMimeType) { + let contentType = aRequest.getHeader("Content-Type"); + do_check_eq(contentType, aExpectedMimeType); + } + + var data = String.fromCharCode.apply(null, bytes); + + do_check_eq(data, aExpectedData); + + aResponse.setStatusLine(null, 200, "OK"); + aResponse.setHeader("Content-Type", "application/json"); + aResponse.write("Success!"); + } +} + +add_test(function test_successCallback() { + do_test_pending(); + let options = { + onLoad: function(aResponse) { + do_check_eq(aResponse, "Success!"); + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + } + } + httpRequest(kSuccessUrl, options); +}); + +add_test(function test_errorCallback() { + do_test_pending(); + let options = { + onSuccess: function(aResponse) { + do_check_true(false); + do_test_finished(); + run_next_test(); + }, + onError: function(e, aResponse) { + do_check_eq(e, "404 - Not Found"); + do_test_finished(); + run_next_test(); + } + } + httpRequest(kBaseUrl + "/failure", options); +}); + +add_test(function test_PostData() { + do_test_pending(); + let options = { + onLoad: function(aResponse) { + do_check_eq(aResponse, "Success!"); + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + }, + postData: kPostDataSent + } + httpRequest(kPostUrl, options); +}); + +add_test(function test_PutData() { + do_test_pending(); + let options = { + method: "PUT", + onLoad: function(aResponse) { + do_check_eq(aResponse, "Success!"); + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + }, + postData: kPutDataSent + } + httpRequest(kPutUrl, options); +}); + +add_test(function test_GetData() { + do_test_pending(); + let options = { + onLoad: function(aResponse) { + do_check_eq(aResponse, "Success!"); + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + }, + postData: null + } + httpRequest(kGetUrl, options); +}); + +add_test(function test_OptionalParameters() { + let options = { + onLoad: null, + onError: null, + logger: null + }; + // Just make sure that nothing throws when doing this (i.e. httpRequest + // doesn't try to access null options). + httpRequest(kGetUrl, options); + run_next_test(); +}); + +/** + * Makes sure that httpRequest API allows setting a custom Content-Type header + * for POST requests when data is a string. + */ +add_test(function test_CustomContentTypeOnPost() { + do_test_pending(); + + // Preparing the request parameters. + let options = { + onLoad: function(aResponse) { + do_check_eq(aResponse, "Success!"); + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + }, + postData: kJsonPostData, + // Setting a custom Content-Type header. + headers: [['Content-Type', "application/json"]] + } + + // Firing the request. + httpRequest(kJsonPostUrl, options); +}); + +/** + * Ensures that the httpRequest API provides a way to override the response + * MIME type. + */ +add_test(function test_OverrideMimeType() { + do_test_pending(); + + // Preparing the request parameters. + const kMimeType = 'text/xml; charset=UTF-8'; + let options = { + onLoad: function(aResponse, xhr) { + do_check_eq(aResponse, "Success!"); + + // Set the expected MIME-type. + let reportedMimeType = xhr.getResponseHeader("Content-Type"); + do_check_neq(reportedMimeType, kMimeType); + + // responseXML should not be not null if overriding mime type succeeded. + do_check_true(xhr.responseXML != null); + + do_test_finished(); + run_next_test(); + }, + onError: function(e) { + do_check_true(false); + do_test_finished(); + run_next_test(); + } + }; + + // Firing the request. + let xhr = httpRequest(kGetUrl, options); + + // Override the response MIME type. + xhr.overrideMimeType(kMimeType); +}); + +function run_test() { + // Set up a mock HTTP server to serve a success page. + server = new HttpServer(); + server.registerPathHandler(kSuccessPath, successResult); + server.registerPathHandler(kPostPath, + getDataChecker("POST", kPostDataReceived, + kPostMimeTypeReceived)); + server.registerPathHandler(kPutPath, + getDataChecker("PUT", kPutDataReceived)); + server.registerPathHandler(kGetPath, getDataChecker("GET", "")); + server.registerPathHandler(kJsonPostPath, + getDataChecker("POST", kJsonPostData, + kJsonPostMimeType)); + + server.start(kDefaultServerPort); + + run_next_test(); + + // Teardown. + do_register_cleanup(function() { + server.stop(function() { }); + }); +} + diff --git a/toolkit/modules/tests/xpcshell/test_Integration.js b/toolkit/modules/tests/xpcshell/test_Integration.js new file mode 100644 index 000000000..808e2d34f --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Integration.js @@ -0,0 +1,238 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Tests the Integration.jsm module. + */ + +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Integration.jsm", this); +Cu.import("resource://gre/modules/Services.jsm", this); +Cu.import("resource://gre/modules/Task.jsm", this); + +const TestIntegration = { + value: "value", + + get valueFromThis() { + return this.value; + }, + + get property() { + return this._property; + }, + + set property(value) { + this._property = value; + }, + + method(argument) { + this.methodArgument = argument; + return "method" + argument; + }, + + asyncMethod: Task.async(function* (argument) { + this.asyncMethodArgument = argument; + return "asyncMethod" + argument; + }), +}; + +let overrideFn = base => ({ + value: "overridden-value", + + get property() { + return "overridden-" + base.__lookupGetter__("property").call(this); + }, + + set property(value) { + base.__lookupSetter__("property").call(this, "overridden-" + value); + }, + + method() { + return "overridden-" + base.method.apply(this, arguments); + }, + + asyncMethod: Task.async(function* () { + return "overridden-" + (yield base.asyncMethod.apply(this, arguments)); + }), +}); + +let superOverrideFn = base => ({ + __proto__: base, + + value: "overridden-value", + + get property() { + return "overridden-" + super.property; + }, + + set property(value) { + super.property = "overridden-" + value; + }, + + method() { + return "overridden-" + super.method(...arguments); + }, + + asyncMethod: Task.async(function* () { + // We cannot use the "super" keyword in methods defined using "Task.async". + return "overridden-" + (yield base.asyncMethod.apply(this, arguments)); + }), +}); + +/** + * Fails the test if the results of method invocations on the combined object + * don't match the expected results based on how many overrides are registered. + * + * @param combined + * The combined object based on the TestIntegration root. + * @param overridesCount + * Zero if the root object is not overridden, or a higher value to test + * the presence of one or more integration overrides. + */ +function* assertCombinedResults(combined, overridesCount) { + let expectedValue = overridesCount > 0 ? "overridden-value" : "value"; + let prefix = "overridden-".repeat(overridesCount); + + Assert.equal(combined.value, expectedValue); + Assert.equal(combined.valueFromThis, expectedValue); + + combined.property = "property"; + Assert.equal(combined.property, prefix.repeat(2) + "property"); + + combined.methodArgument = ""; + Assert.equal(combined.method("-argument"), prefix + "method-argument"); + Assert.equal(combined.methodArgument, "-argument"); + + combined.asyncMethodArgument = ""; + Assert.equal(yield combined.asyncMethod("-argument"), + prefix + "asyncMethod-argument"); + Assert.equal(combined.asyncMethodArgument, "-argument"); +} + +/** + * Fails the test if the results of method invocations on the combined object + * for the "testModule" integration point don't match the expected results based + * on how many overrides are registered. + * + * @param overridesCount + * Zero if the root object is not overridden, or a higher value to test + * the presence of one or more integration overrides. + */ +function* assertCurrentCombinedResults(overridesCount) { + let combined = Integration.testModule.getCombined(TestIntegration); + yield assertCombinedResults(combined, overridesCount); +} + +/** + * Checks the initial state with no integration override functions registered. + */ +add_task(function* test_base() { + yield assertCurrentCombinedResults(0); +}); + +/** + * Registers and unregisters an integration override function. + */ +add_task(function* test_override() { + Integration.testModule.register(overrideFn); + yield assertCurrentCombinedResults(1); + + // Registering the same function more than once has no effect. + Integration.testModule.register(overrideFn); + yield assertCurrentCombinedResults(1); + + Integration.testModule.unregister(overrideFn); + yield assertCurrentCombinedResults(0); +}); + +/** + * Registers and unregisters more than one integration override function, of + * which one uses the prototype and the "super" keyword to access the base. + */ +add_task(function* test_override_super_multiple() { + Integration.testModule.register(overrideFn); + Integration.testModule.register(superOverrideFn); + yield assertCurrentCombinedResults(2); + + Integration.testModule.unregister(overrideFn); + yield assertCurrentCombinedResults(1); + + Integration.testModule.unregister(superOverrideFn); + yield assertCurrentCombinedResults(0); +}); + +/** + * Registers an integration override function that throws an exception, and + * ensures that this does not block other functions from being registered. + */ +add_task(function* test_override_error() { + let errorOverrideFn = base => { throw "Expected error." }; + + Integration.testModule.register(errorOverrideFn); + Integration.testModule.register(overrideFn); + yield assertCurrentCombinedResults(1); + + Integration.testModule.unregister(errorOverrideFn); + Integration.testModule.unregister(overrideFn); + yield assertCurrentCombinedResults(0); +}); + +/** + * Checks that state saved using the "this" reference is preserved as a shallow + * copy when registering new integration override functions. + */ +add_task(function* test_state_preserved() { + let valueObject = { toString: () => "toString" }; + + let combined = Integration.testModule.getCombined(TestIntegration); + combined.property = valueObject; + Assert.ok(combined.property === valueObject); + + Integration.testModule.register(overrideFn); + combined = Integration.testModule.getCombined(TestIntegration); + Assert.equal(combined.property, "overridden-toString"); + + Integration.testModule.unregister(overrideFn); + combined = Integration.testModule.getCombined(TestIntegration); + Assert.ok(combined.property === valueObject); +}); + +/** + * Checks that the combined integration objects cannot be used with XPCOM. + * + * This is limited by the fact that interfaces with the "[function]" annotation, + * for example nsIObserver, do not call the QueryInterface implementation. + */ +add_task(function* test_xpcom_throws() { + let combined = Integration.testModule.getCombined(TestIntegration); + + // This calls QueryInterface because it looks for nsISupportsWeakReference. + Assert.throws(() => Services.obs.addObserver(combined, "test-topic", true), + "NS_NOINTERFACE"); +}); + +/** + * Checks that getters defined by defineModuleGetter are able to retrieve the + * latest version of the combined integration object. + */ +add_task(function* test_defineModuleGetter() { + let objectForGetters = {}; + + // Test with and without the optional "symbol" parameter. + Integration.testModule.defineModuleGetter(objectForGetters, + "TestIntegration", "resource://testing-common/TestIntegration.jsm"); + Integration.testModule.defineModuleGetter(objectForGetters, + "integration", "resource://testing-common/TestIntegration.jsm", + "TestIntegration"); + + Integration.testModule.register(overrideFn); + yield assertCombinedResults(objectForGetters.integration, 1); + yield assertCombinedResults(objectForGetters.TestIntegration, 1); + + Integration.testModule.unregister(overrideFn); + yield assertCombinedResults(objectForGetters.integration, 0); + yield assertCombinedResults(objectForGetters.TestIntegration, 0); +}); diff --git a/toolkit/modules/tests/xpcshell/test_JSONFile.js b/toolkit/modules/tests/xpcshell/test_JSONFile.js new file mode 100644 index 000000000..77e8c55b9 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_JSONFile.js @@ -0,0 +1,242 @@ +/** + * Tests the JSONFile object. + */ + +"use strict"; + +// Globals + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths", + "resource://gre/modules/DownloadPaths.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "JSONFile", + "resource://gre/modules/JSONFile.jsm"); + +let gFileCounter = Math.floor(Math.random() * 1000000); + +/** + * Returns a reference to a temporary file, that is guaranteed not to exist, and + * to have never been created before. + * + * @param aLeafName + * Suggested leaf name for the file to be created. + * + * @return nsIFile pointing to a non-existent file in a temporary directory. + * + * @note It is not enough to delete the file if it exists, or to delete the file + * after calling nsIFile.createUnique, because on Windows the delete + * operation in the file system may still be pending, preventing a new + * file with the same name to be created. + */ +function getTempFile(aLeafName) +{ + // Prepend a serial number to the extension in the suggested leaf name. + let [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName); + let leafName = base + "-" + gFileCounter + ext; + gFileCounter++; + + // Get a file reference under the temporary directory for this test file. + let file = FileUtils.getFile("TmpD", [leafName]); + do_check_false(file.exists()); + + do_register_cleanup(function () { + if (file.exists()) { + file.remove(false); + } + }); + + return file; +} + +const TEST_STORE_FILE_NAME = "test-store.json"; + +const TEST_DATA = { + number: 123, + string: "test", + object: { + prop1: 1, + prop2: 2, + }, +}; + +// Tests + +add_task(function* test_save_reload() +{ + let storeForSave = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + }); + + yield storeForSave.load(); + + do_check_true(storeForSave.dataReady); + do_check_matches(storeForSave.data, {}); + + Object.assign(storeForSave.data, TEST_DATA); + + yield new Promise((resolve) => { + let save = storeForSave._save.bind(storeForSave); + storeForSave._save = () => { + save(); + resolve(); + }; + storeForSave.saveSoon(); + }); + + let storeForLoad = new JSONFile({ + path: storeForSave.path, + }); + + yield storeForLoad.load(); + + Assert.deepEqual(storeForLoad.data, TEST_DATA); +}); + +add_task(function* test_load_sync() +{ + let storeForSave = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path + }); + yield storeForSave.load(); + Object.assign(storeForSave.data, TEST_DATA); + yield storeForSave._save(); + + let storeForLoad = new JSONFile({ + path: storeForSave.path, + }); + storeForLoad.ensureDataReady(); + + Assert.deepEqual(storeForLoad.data, TEST_DATA); +}); + +add_task(function* test_load_with_dataPostProcessor() +{ + let storeForSave = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path + }); + yield storeForSave.load(); + Object.assign(storeForSave.data, TEST_DATA); + yield storeForSave._save(); + + let random = Math.random(); + let storeForLoad = new JSONFile({ + path: storeForSave.path, + dataPostProcessor: (data) => { + Assert.deepEqual(data, TEST_DATA); + + data.test = random; + return data; + }, + }); + + yield storeForLoad.load(); + + do_check_eq(storeForLoad.data.test, random); +}); + +add_task(function* test_load_with_dataPostProcessor_fails() +{ + let store = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + dataPostProcessor: () => { + throw new Error("dataPostProcessor fails."); + }, + }); + + yield Assert.rejects(store.load(), /dataPostProcessor fails\./); + + do_check_false(store.dataReady); +}); + +add_task(function* test_load_sync_with_dataPostProcessor_fails() +{ + let store = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + dataPostProcessor: () => { + throw new Error("dataPostProcessor fails."); + }, + }); + + Assert.throws(() => store.ensureDataReady(), /dataPostProcessor fails\./); + + do_check_false(store.dataReady); +}); + +/** + * Loads data from a string in a predefined format. The purpose of this test is + * to verify that the JSON format used in previous versions can be loaded. + */ +add_task(function* test_load_string_predefined() +{ + let store = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + }); + + let string = + "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,\"prop2\":2}}"; + + yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + yield store.load(); + + Assert.deepEqual(store.data, TEST_DATA); +}); + +/** + * Loads data from a malformed JSON string. + */ +add_task(function* test_load_string_malformed() +{ + let store = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + }); + + let string = "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,"; + + yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + yield store.load(); + + // A backup file should have been created. + do_check_true(yield OS.File.exists(store.path + ".corrupt")); + yield OS.File.remove(store.path + ".corrupt"); + + // The store should be ready to accept new data. + do_check_true(store.dataReady); + do_check_matches(store.data, {}); +}); + +/** + * Loads data from a malformed JSON string, using the synchronous initialization + * path. + */ +add_task(function* test_load_string_malformed_sync() +{ + let store = new JSONFile({ + path: getTempFile(TEST_STORE_FILE_NAME).path, + }); + + let string = "{\"number\":123,\"string\":\"test\",\"object\":{\"prop1\":1,"; + + yield OS.File.writeAtomic(store.path, new TextEncoder().encode(string), + { tmpPath: store.path + ".tmp" }); + + store.ensureDataReady(); + + // A backup file should have been created. + do_check_true(yield OS.File.exists(store.path + ".corrupt")); + yield OS.File.remove(store.path + ".corrupt"); + + // The store should be ready to accept new data. + do_check_true(store.dataReady); + do_check_matches(store.data, {}); +}); diff --git a/toolkit/modules/tests/xpcshell/test_Log.js b/toolkit/modules/tests/xpcshell/test_Log.js new file mode 100644 index 000000000..429bbcc50 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Log.js @@ -0,0 +1,592 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable block-spacing */ + +var {utils: Cu} = Components; + +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); + +Cu.import("resource://gre/modules/Promise.jsm") +Cu.import("resource://gre/modules/Log.jsm"); + +var testFormatter = { + format: function format(message) { + return message.loggerName + "\t" + + message.levelDesc + "\t" + + message.message; + } +}; + +function MockAppender(formatter) { + Log.Appender.call(this, formatter); + this.messages = []; +} +MockAppender.prototype = { + __proto__: Log.Appender.prototype, + + doAppend: function DApp_doAppend(message) { + this.messages.push(message); + } +}; + +function run_test() { + run_next_test(); +} + +add_task(function test_Logger() { + let log = Log.repository.getLogger("test.logger"); + let appender = new MockAppender(new Log.BasicFormatter()); + + log.level = Log.Level.Debug; + appender.level = Log.Level.Info; + log.addAppender(appender); + log.info("info test"); + log.debug("this should be logged but not appended."); + + do_check_eq(appender.messages.length, 1); + + let msgRe = /\d+\ttest.logger\t\INFO\tinfo test/; + do_check_true(msgRe.test(appender.messages[0])); +}); + +add_task(function test_Logger_parent() { + // Check whether parenting is correct + let grandparentLog = Log.repository.getLogger("grandparent"); + let childLog = Log.repository.getLogger("grandparent.parent.child"); + do_check_eq(childLog.parent.name, "grandparent"); + + let parentLog = Log.repository.getLogger("grandparent.parent"); + do_check_eq(childLog.parent.name, "grandparent.parent"); + + // Check that appends are exactly in scope + let gpAppender = new MockAppender(new Log.BasicFormatter()); + gpAppender.level = Log.Level.Info; + grandparentLog.addAppender(gpAppender); + childLog.info("child info test"); + Log.repository.rootLogger.info("this shouldn't show up in gpAppender"); + + do_check_eq(gpAppender.messages.length, 1); + do_check_true(gpAppender.messages[0].indexOf("child info test") > 0); +}); + +add_test(function test_LoggerWithMessagePrefix() { + let log = Log.repository.getLogger("test.logger.prefix"); + let appender = new MockAppender(new Log.MessageOnlyFormatter()); + log.addAppender(appender); + + let prefixed = Log.repository.getLoggerWithMessagePrefix( + "test.logger.prefix", "prefix: "); + + log.warn("no prefix"); + prefixed.warn("with prefix"); + + Assert.equal(appender.messages.length, 2, "2 messages were logged."); + Assert.deepEqual(appender.messages, [ + "no prefix", + "prefix: with prefix", + ], "Prefix logger works."); + + run_next_test(); +}); + +/* + * A utility method for checking object equivalence. + * Fields with a reqular expression value in expected will be tested + * against the corresponding value in actual. Otherwise objects + * are expected to have the same keys and equal values. + */ +function checkObjects(expected, actual) { + do_check_true(expected instanceof Object); + do_check_true(actual instanceof Object); + for (let key in expected) { + do_check_neq(actual[key], undefined); + if (expected[key] instanceof RegExp) { + do_check_true(expected[key].test(actual[key].toString())); + } else if (expected[key] instanceof Object) { + checkObjects(expected[key], actual[key]); + } else { + do_check_eq(expected[key], actual[key]); + } + } + + for (let key in actual) { + do_check_neq(expected[key], undefined); + } +} + +add_task(function test_StructuredLogCommands() { + let appender = new MockAppender(new Log.StructuredFormatter()); + let logger = Log.repository.getLogger("test.StructuredOutput"); + logger.addAppender(appender); + logger.level = Log.Level.Info; + + logger.logStructured("test_message", {_message: "message string one"}); + logger.logStructured("test_message", {_message: "message string two", + _level: "ERROR", + source_file: "test_Log.js"}); + logger.logStructured("test_message"); + logger.logStructured("test_message", {source_file: "test_Log.js", + message_position: 4}); + + let messageOne = {"_time": /\d+/, + "_namespace": "test.StructuredOutput", + "_level": "INFO", + "_message": "message string one", + "action": "test_message"}; + + let messageTwo = {"_time": /\d+/, + "_namespace": "test.StructuredOutput", + "_level": "ERROR", + "_message": "message string two", + "action": "test_message", + "source_file": "test_Log.js"}; + + let messageThree = {"_time": /\d+/, + "_namespace": "test.StructuredOutput", + "_level": "INFO", + "action": "test_message"}; + + let messageFour = {"_time": /\d+/, + "_namespace": "test.StructuredOutput", + "_level": "INFO", + "action": "test_message", + "source_file": "test_Log.js", + "message_position": 4}; + + checkObjects(messageOne, JSON.parse(appender.messages[0])); + checkObjects(messageTwo, JSON.parse(appender.messages[1])); + checkObjects(messageThree, JSON.parse(appender.messages[2])); + checkObjects(messageFour, JSON.parse(appender.messages[3])); + + let errored = false; + try { + logger.logStructured("", {_message: "invalid message"}); + } catch (e) { + errored = true; + do_check_eq(e, "An action is required when logging a structured message."); + } finally { + do_check_true(errored); + } + + errored = false; + try { + logger.logStructured("message_action", "invalid params"); + } catch (e) { + errored = true; + do_check_eq(e, "The params argument is required to be an object."); + } finally { + do_check_true(errored); + } + + // Logging with unstructured interface should produce the same messages + // as the structured interface for these cases. + appender = new MockAppender(new Log.StructuredFormatter()); + logger = Log.repository.getLogger("test.StructuredOutput1"); + messageOne._namespace = "test.StructuredOutput1"; + messageTwo._namespace = "test.StructuredOutput1"; + logger.addAppender(appender); + logger.level = Log.Level.All; + logger.info("message string one", {action: "test_message"}); + logger.error("message string two", {action: "test_message", + source_file: "test_Log.js"}); + + checkObjects(messageOne, JSON.parse(appender.messages[0])); + checkObjects(messageTwo, JSON.parse(appender.messages[1])); +}); + +add_task(function test_StorageStreamAppender() { + let appender = new Log.StorageStreamAppender(testFormatter); + do_check_eq(appender.getInputStream(), null); + + // Log to the storage stream and verify the log was written and can be + // read back. + let logger = Log.repository.getLogger("test.StorageStreamAppender"); + logger.addAppender(appender); + logger.info("OHAI"); + let inputStream = appender.getInputStream(); + let data = NetUtil.readInputStreamToString(inputStream, + inputStream.available()); + do_check_eq(data, "test.StorageStreamAppender\tINFO\tOHAI\n"); + + // We can read it again even. + let sndInputStream = appender.getInputStream(); + let sameData = NetUtil.readInputStreamToString(sndInputStream, + sndInputStream.available()); + do_check_eq(data, sameData); + + // Reset the appender and log some more. + appender.reset(); + do_check_eq(appender.getInputStream(), null); + logger.debug("wut?!?"); + inputStream = appender.getInputStream(); + data = NetUtil.readInputStreamToString(inputStream, + inputStream.available()); + do_check_eq(data, "test.StorageStreamAppender\tDEBUG\twut?!?\n"); +}); + +function fileContents(path) { + let decoder = new TextDecoder(); + return OS.File.read(path).then(array => { + return decoder.decode(array); + }); +} + +add_task(function* test_FileAppender() { + // This directory does not exist yet + let dir = OS.Path.join(do_get_profile().path, "test_Log"); + do_check_false(yield OS.File.exists(dir)); + let path = OS.Path.join(dir, "test_FileAppender"); + let appender = new Log.FileAppender(path, testFormatter); + let logger = Log.repository.getLogger("test.FileAppender"); + logger.addAppender(appender); + + // Logging to a file that can't be created won't do harm. + do_check_false(yield OS.File.exists(path)); + logger.info("OHAI!"); + + yield OS.File.makeDir(dir); + logger.info("OHAI"); + yield appender._lastWritePromise; + + do_check_eq((yield fileContents(path)), + "test.FileAppender\tINFO\tOHAI\n"); + + logger.info("OHAI"); + yield appender._lastWritePromise; + + do_check_eq((yield fileContents(path)), + "test.FileAppender\tINFO\tOHAI\n" + + "test.FileAppender\tINFO\tOHAI\n"); + + // Reset the appender and log some more. + yield appender.reset(); + do_check_false(yield OS.File.exists(path)); + + logger.debug("O RLY?!?"); + yield appender._lastWritePromise; + do_check_eq((yield fileContents(path)), + "test.FileAppender\tDEBUG\tO RLY?!?\n"); + + yield appender.reset(); + logger.debug("1"); + logger.info("2"); + logger.info("3"); + logger.info("4"); + logger.info("5"); + // Waiting on only the last promise should account for all of these. + yield appender._lastWritePromise; + + // Messages ought to be logged in order. + do_check_eq((yield fileContents(path)), + "test.FileAppender\tDEBUG\t1\n" + + "test.FileAppender\tINFO\t2\n" + + "test.FileAppender\tINFO\t3\n" + + "test.FileAppender\tINFO\t4\n" + + "test.FileAppender\tINFO\t5\n"); +}); + +add_task(function* test_BoundedFileAppender() { + let dir = OS.Path.join(do_get_profile().path, "test_Log"); + + if (!(yield OS.File.exists(dir))) { + yield OS.File.makeDir(dir); + } + + let path = OS.Path.join(dir, "test_BoundedFileAppender"); + // This appender will hold about two lines at a time. + let appender = new Log.BoundedFileAppender(path, testFormatter, 40); + let logger = Log.repository.getLogger("test.BoundedFileAppender"); + logger.addAppender(appender); + + logger.info("ONE"); + logger.info("TWO"); + yield appender._lastWritePromise; + + do_check_eq((yield fileContents(path)), + "test.BoundedFileAppender\tINFO\tONE\n" + + "test.BoundedFileAppender\tINFO\tTWO\n"); + + logger.info("THREE"); + logger.info("FOUR"); + + do_check_neq(appender._removeFilePromise, undefined); + yield appender._removeFilePromise; + yield appender._lastWritePromise; + + do_check_eq((yield fileContents(path)), + "test.BoundedFileAppender\tINFO\tTHREE\n" + + "test.BoundedFileAppender\tINFO\tFOUR\n"); + + yield appender.reset(); + logger.info("ONE"); + logger.info("TWO"); + logger.info("THREE"); + logger.info("FOUR"); + + do_check_neq(appender._removeFilePromise, undefined); + yield appender._removeFilePromise; + yield appender._lastWritePromise; + + do_check_eq((yield fileContents(path)), + "test.BoundedFileAppender\tINFO\tTHREE\n" + + "test.BoundedFileAppender\tINFO\tFOUR\n"); + +}); + +/* + * Test parameter formatting. + */ +add_task(function* log_message_with_params() { + let formatter = new Log.BasicFormatter(); + + function formatMessage(text, params) { + let full = formatter.format(new Log.LogMessage("test.logger", Log.Level.Warn, text, params)); + return full.split("\t")[3]; + } + + // Strings are substituted directly. + do_check_eq(formatMessage("String is ${foo}", {foo: "bar"}), + "String is bar"); + + // Numbers are substituted. + do_check_eq(formatMessage("Number is ${number}", {number: 47}), + "Number is 47") + + // The entire params object is JSON-formatted and substituted. + do_check_eq(formatMessage("Object is ${}", {foo: "bar"}), + 'Object is {"foo":"bar"}'); + + // An object nested inside params is JSON-formatted and substituted. + do_check_eq(formatMessage("Sub object is ${sub}", {sub: {foo: "bar"}}), + 'Sub object is {"foo":"bar"}'); + + // The substitution field is missing from params. Leave the placeholder behind + // to make the mistake obvious. + do_check_eq(formatMessage("Missing object is ${missing}", {}), + 'Missing object is ${missing}'); + + // Make sure we don't treat the parameter name 'false' as a falsey value. + do_check_eq(formatMessage("False is ${false}", {false: true}), + 'False is true'); + + // If an object has a .toJSON method, the formatter uses it. + let ob = function() {}; + ob.toJSON = function() {return {sneaky: "value"}}; + do_check_eq(formatMessage("JSON is ${sub}", {sub: ob}), + 'JSON is {"sneaky":"value"}'); + + // Fall back to .toSource() if JSON.stringify() fails on an object. + ob = function() {}; + ob.toJSON = function() {throw "oh noes JSON"}; + do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}), + 'Fail is (function () {})'); + + // Fall back to .toString if both .toJSON and .toSource fail. + ob.toSource = function() {throw "oh noes SOURCE"}; + do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}), + 'Fail is function () {}'); + + // Fall back to '[object]' if .toJSON, .toSource and .toString fail. + ob.toString = function() {throw "oh noes STRING"}; + do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}), + 'Fail is [object]'); + + // If params are passed but there are no substitution in the text + // we JSON format and append the entire parameters object. + do_check_eq(formatMessage("Text with no subs", {a: "b", c: "d"}), + 'Text with no subs: {"a":"b","c":"d"}'); + + // If we substitute one parameter but not the other, + // we ignore any params that aren't substituted. + do_check_eq(formatMessage("Text with partial sub ${a}", {a: "b", c: "d"}), + 'Text with partial sub b'); + + // We don't format internal fields stored in params. + do_check_eq(formatMessage("Params with _ ${}", {a: "b", _c: "d", _level:20, _message:"froo", + _time:123456, _namespace:"here.there"}), + 'Params with _ {"a":"b","_c":"d"}'); + + // Don't print an empty params holder if all params are internal. + do_check_eq(formatMessage("All params internal", {_level:20, _message:"froo", + _time:123456, _namespace:"here.there"}), + 'All params internal'); + + // Format params with null and undefined values. + do_check_eq(formatMessage("Null ${n} undefined ${u}", {n: null, u: undefined}), + 'Null null undefined undefined'); + + // Format params with number, bool, and Object/String type. + do_check_eq(formatMessage("number ${n} boolean ${b} boxed Boolean ${bx} String ${s}", + {n: 45, b: false, bx: new Boolean(true), s: new String("whatevs")}), + 'number 45 boolean false boxed Boolean true String whatevs'); + + /* + * Check that errors get special formatting if they're formatted directly as + * a named param or they're the only param, but not if they're a field in a + * larger structure. + */ + let err = Components.Exception("test exception", Components.results.NS_ERROR_FAILURE); + let str = formatMessage("Exception is ${}", err); + do_check_true(str.includes('Exception is [Exception... "test exception"')); + do_check_true(str.includes("(NS_ERROR_FAILURE)")); + str = formatMessage("Exception is", err); + do_check_true(str.includes('Exception is: [Exception... "test exception"')); + str = formatMessage("Exception is ${error}", {error: err}); + do_check_true(str.includes('Exception is [Exception... "test exception"')); + str = formatMessage("Exception is", {_error: err}); + do_print(str); + // Exceptions buried inside objects are formatted badly. + do_check_true(str.includes('Exception is: {"_error":{}')); + // If the message text is null, the message contains only the formatted params object. + str = formatMessage(null, err); + do_check_true(str.startsWith('[Exception... "test exception"')); + // If the text is null and 'params' is a String object, the message is exactly that string. + str = formatMessage(null, new String("String in place of params")); + do_check_eq(str, "String in place of params"); + + // We use object.valueOf() internally; make sure a broken valueOf() method + // doesn't cause the logger to fail. + let vOf = {a: 1, valueOf: function() {throw "oh noes valueOf"}}; + do_check_eq(formatMessage("Broken valueOf ${}", vOf), + 'Broken valueOf ({a:1, valueOf:(function () {throw "oh noes valueOf"})})'); + + // Test edge cases of bad data to formatter: + // If 'params' is not an object, format it as a basic type. + do_check_eq(formatMessage("non-object no subst", 1), + 'non-object no subst: 1'); + do_check_eq(formatMessage("non-object all subst ${}", 2), + 'non-object all subst 2'); + do_check_eq(formatMessage("false no subst", false), + 'false no subst: false'); + do_check_eq(formatMessage("null no subst", null), + 'null no subst: null'); + // If 'params' is undefined and there are no substitutions expected, + // the message should still be output. + do_check_eq(formatMessage("undefined no subst", undefined), + 'undefined no subst'); + // If 'params' is not an object, no named substitutions can succeed; + // therefore we leave the placeholder and append the formatted params. + do_check_eq(formatMessage("non-object named subst ${junk} space", 3), + 'non-object named subst ${junk} space: 3'); + // If there are no params, we leave behind the placeholders in the text. + do_check_eq(formatMessage("no params ${missing}", undefined), + 'no params ${missing}'); + // If params doesn't contain any of the tags requested in the text, + // we leave them all behind and append the formatted params. + do_check_eq(formatMessage("object missing tag ${missing} space", {mising: "not here"}), + 'object missing tag ${missing} space: {"mising":"not here"}'); + // If we are given null text and no params, the resulting formatted message is empty. + do_check_eq(formatMessage(null), ''); +}); + +/* + * If we call a log function with a non-string object in place of the text + * argument, and no parameters, treat that the same as logging empty text + * with the object argument as parameters. This makes the log useful when the + * caller does "catch(err) {logger.error(err)}" + */ +add_task(function* test_log_err_only() { + let log = Log.repository.getLogger("error.only"); + let mockFormatter = { format: msg => msg }; + let appender = new MockAppender(mockFormatter); + log.addAppender(appender); + + /* + * Check that log.error(err) is treated the same as + * log.error(null, err) by the logMessage constructor; the formatMessage() + * tests above ensure that the combination of null text and an error object + * is formatted correctly. + */ + try { + eval("javascript syntax error"); + } + catch (e) { + log.error(e); + msg = appender.messages.pop(); + do_check_eq(msg.message, null); + do_check_eq(msg.params, e); + } +}); + +/* + * Test logStructured() messages through basic formatter. + */ +add_task(function* test_structured_basic() { + let log = Log.repository.getLogger("test.logger"); + let appender = new MockAppender(new Log.BasicFormatter()); + + log.level = Log.Level.Info; + appender.level = Log.Level.Info; + log.addAppender(appender); + + // A structured entry with no _message is treated the same as log./level/(null, params) + // except the 'action' field is added to the object. + log.logStructured("action", {data: "structure"}); + do_check_eq(appender.messages.length, 1); + do_check_true(appender.messages[0].includes('{"data":"structure","action":"action"}')); + + // A structured entry with _message and substitution is treated the same as + // log./level/(null, params). + log.logStructured("action", {_message: "Structured sub ${data}", data: "structure"}); + do_check_eq(appender.messages.length, 2); + do_print(appender.messages[1]); + do_check_true(appender.messages[1].includes('Structured sub structure')); +}); + +/* + * Test that all the basic logger methods pass the message and params through to the appender. + */ +add_task(function* log_message_with_params() { + let log = Log.repository.getLogger("error.logger"); + let mockFormatter = { format: msg => msg }; + let appender = new MockAppender(mockFormatter); + log.addAppender(appender); + + let testParams = {a:1, b:2}; + log.fatal("Test fatal", testParams); + log.error("Test error", testParams); + log.warn("Test warn", testParams); + log.info("Test info", testParams); + log.config("Test config", testParams); + log.debug("Test debug", testParams); + log.trace("Test trace", testParams); + do_check_eq(appender.messages.length, 7); + for (let msg of appender.messages) { + do_check_true(msg.params === testParams); + do_check_true(msg.message.startsWith("Test ")); + } +}); + +/* + * Check that we format JS Errors reasonably. + */ +add_task(function* format_errors() { + let pFormat = new Log.ParameterFormatter(); + + // Test that subclasses of Error are recognized as errors. + err = new ReferenceError("Ref Error", "ERROR_FILE", 28); + str = pFormat.format(err); + do_check_true(str.includes("ReferenceError")); + do_check_true(str.includes("ERROR_FILE:28")); + do_check_true(str.includes("Ref Error")); + + // Test that JS-generated Errors are recognized and formatted. + try { + yield Promise.resolve(); // Scrambles the stack + eval("javascript syntax error"); + } + catch (e) { + str = pFormat.format(e); + do_check_true(str.includes("SyntaxError: missing ;")); + // Make sure we identified it as an Error and formatted the error location as + // lineNumber:columnNumber. + do_check_true(str.includes(":1:11)")); + // Make sure that we use human-readable stack traces + // Check that the error doesn't contain any reference to "Promise.jsm" or "Task.jsm" + do_check_false(str.includes("Promise.jsm")); + do_check_false(str.includes("Task.jsm")); + do_check_true(str.includes("format_errors")); + } +}); diff --git a/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js b/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js new file mode 100644 index 000000000..6e53db058 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Log_stackTrace.js @@ -0,0 +1,30 @@ +print("Define some functions in well defined line positions for the test"); +function foo(v) { return bar(v + 1); } // line 2 +function bar(v) { return baz(v + 1); } // line 3 +function baz(v) { throw new Error(v + 1); } // line 4 + +print("Make sure lazy constructor calling/assignment works"); +Components.utils.import("resource://gre/modules/Log.jsm"); + +function run_test() { + print("Make sure functions, arguments, files are pretty printed in the trace"); + let trace = ""; + try { + foo(0); + } + catch (ex) { + trace = Log.stackTrace(ex); + } + print(`Got trace: ${trace}`); + do_check_neq(trace, ""); + + let bazPos = trace.indexOf("baz@test_Log_stackTrace.js:4"); + let barPos = trace.indexOf("bar@test_Log_stackTrace.js:3"); + let fooPos = trace.indexOf("foo@test_Log_stackTrace.js:2"); + print(`String positions: ${bazPos} ${barPos} ${fooPos}`); + + print("Make sure the desired messages show up"); + do_check_true(bazPos >= 0); + do_check_true(barPos > bazPos); + do_check_true(fooPos > barPos); +} diff --git a/toolkit/modules/tests/xpcshell/test_MatchGlobs.js b/toolkit/modules/tests/xpcshell/test_MatchGlobs.js new file mode 100644 index 000000000..5dcfd19cb --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_MatchGlobs.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Components.utils.import("resource://gre/modules/MatchPattern.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test(url, pattern) { + let uri = Services.io.newURI(url, null, null); + let m = new MatchGlobs(pattern); + return m.matches(uri.spec); +} + +function pass({url, pattern}) { + ok(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`); +} + +function fail({url, pattern}) { + ok(!test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`); +} + +function run_test() { + let moz = "http://mozilla.org"; + + pass({url: moz, pattern: ["*"]}); + pass({url: moz, pattern: ["http://*"]}), + pass({url: moz, pattern: ["*mozilla*"]}); + pass({url: moz, pattern: ["*example*", "*mozilla*"]}); + + pass({url: moz, pattern: ["*://*"]}); + pass({url: "https://mozilla.org", pattern: ["*://*"]}); + + // Documentation example + pass({url: "http://www.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]}); + pass({url: "http://the.example.com/foo/", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://my.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://example.com/foo/", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://www.example.com/foo", pattern: ["http://???.example.com/foo/*"]}); + + // Matches path + let path = moz + "/abc/def"; + pass({url: path, pattern: ["*def"]}); + pass({url: path, pattern: ["*c/d*"]}); + pass({url: path, pattern: ["*org/abc*"]}); + fail({url: path + "/", pattern: ["*def"]}); + + // Trailing slash + pass({url: moz, pattern: ["*.org/"]}); + fail({url: moz, pattern: ["*.org"]}); + + // Wrong TLD + fail({url: moz, pattern: ["www*.m*.com/"]}); + // Case sensitive + fail({url: moz, pattern: ["*.ORG/"]}); + + fail({url: moz, pattern: []}); +} diff --git a/toolkit/modules/tests/xpcshell/test_MatchPattern.js b/toolkit/modules/tests/xpcshell/test_MatchPattern.js new file mode 100644 index 000000000..583038361 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_MatchPattern.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Components.utils.import("resource://gre/modules/MatchPattern.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test(url, pattern) +{ + let uri = Services.io.newURI(url, null, null); + let m = new MatchPattern(pattern); + return m.matches(uri); +} + +function pass({url, pattern}) +{ + do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`); +} + +function fail({url, pattern}) +{ + do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`); +} + +function run_test() +{ + // Invalid pattern. + fail({url: "http://mozilla.org", pattern: ""}); + + // Pattern must include trailing slash. + fail({url: "http://mozilla.org", pattern: "http://mozilla.org"}); + + // Protocol not allowed. + fail({url: "http://mozilla.org", pattern: "gopher://wuarchive.wustl.edu/"}); + + pass({url: "http://mozilla.org", pattern: "http://mozilla.org/"}); + pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/"}); + + pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/"}); + pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/"}); + fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/"}); + fail({url: "ftp://mozilla.org/", pattern: "*://mozilla.org/"}); + + fail({url: "http://mozilla.com", pattern: "http://*mozilla.com*/"}); + fail({url: "http://mozilla.com", pattern: "http://mozilla.*/"}); + fail({url: "http://mozilla.com", pattern: "http:/mozilla.com/"}); + + pass({url: "http://google.com", pattern: "http://*.google.com/"}); + pass({url: "http://docs.google.com", pattern: "http://*.google.com/"}); + + pass({url: "http://mozilla.org:8080", pattern: "http://mozilla.org/"}); + pass({url: "http://mozilla.org:8080", pattern: "*://mozilla.org/"}); + fail({url: "http://mozilla.org:8080", pattern: "http://mozilla.org:8080/"}); + + // Now try with * in the path. + pass({url: "http://mozilla.org", pattern: "http://mozilla.org/*"}); + pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/*"}); + + pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/*"}); + pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/*"}); + fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/*"}); + fail({url: "http://mozilla.com", pattern: "http://mozilla.*/*"}); + + pass({url: "http://google.com", pattern: "http://*.google.com/*"}); + pass({url: "http://docs.google.com", pattern: "http://*.google.com/*"}); + + // Check path stuff. + fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/"}); + pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*"}); + pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*f"}); + pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*"}); + pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*f"}); + fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*e"}); + fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*c"}); + + fail({url: "http:///a.html", pattern: "http:///a.html"}); + pass({url: "file:///foo", pattern: "file:///foo*"}); + pass({url: "file:///foo/bar.html", pattern: "file:///foo*"}); + + pass({url: "http://mozilla.org/a", pattern: ""}); + pass({url: "https://mozilla.org/a", pattern: ""}); + pass({url: "ftp://mozilla.org/a", pattern: ""}); + pass({url: "file:///a", pattern: ""}); + fail({url: "gopher://wuarchive.wustl.edu/a", pattern: ""}); + + // Multiple patterns. + pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]}); + pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); + pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); + fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); + + // Match url with fragments. + pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"}); +} diff --git a/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js b/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js new file mode 100644 index 000000000..52e03a6cc --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_MatchURLFilters.js @@ -0,0 +1,396 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Components.utils.import("resource://gre/modules/MatchPattern.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function createTestFilter({url, filters}) { + let m = new MatchURLFilters(filters); + return m.matches(url); +} + +function expectPass({url, filters}) { + ok(createTestFilter({url, filters}), + `Expected match: ${JSON.stringify(filters)}, ${url}`); +} + +function expectFail({url, filters}) { + ok(!createTestFilter({url, filters}), + `Expected no match: ${JSON.stringify(filters)}, ${url}`); +} + +function expectThrow({url, filters, exceptionMessageContains}) { + let logData = {filters, url}; + + Assert.throws( + () => { + createTestFilter({url, filters}); + }, + exceptionMessageContains, + `Check received exception for expected message: ${JSON.stringify(logData)}` + ); +} + +add_task(function* test_match_url_filters() { + const shouldPass = true; + const shouldFail = true; + const shouldThrow = true; + + var testCases = [ + // Empty, undefined and null filters. + {shouldThrow, exceptionMessageContains: "filters array should not be empty", + filters: [], url: "http://mozilla.org", }, + {shouldThrow, exceptionMessageContains: "filters should be an array", + filters: undefined, url: "http://mozilla.org"}, + {shouldThrow, exceptionMessageContains: "filters should be an array", + filters: null, url: "http://mozilla.org"}, + + // Wrong formats (in a real webextension this will be blocked by the schema validation). + {shouldThrow, exceptionMessageContains: "filters should be an array", filters: {}, + url: "http://mozilla.org"}, + {shouldThrow, exceptionMessageContains: "filters should be an array", + filters: {nonExistentCriteria: true}, url: "http://mozilla.org", }, + {shouldPass, filters: [{nonExistentCriteria: true}], url: "http://mozilla.org"}, + + // Schemes filter over various url schemes. + {shouldPass, filters: [{schemes: ["http"]}], url: "http://mozilla.org"}, + {shouldPass, filters: [{schemes: ["https"]}], url: "https://mozilla.org"}, + {shouldPass, filters: [{schemes: ["ftp"]}], url: "ftp://fake/ftp/url"}, + {shouldPass, filters: [{schemes: ["about"]}], url: "about:blank"}, + {shouldPass, filters: [{schemes: ["data"]}], url: "data:,testDataURL"}, + {shouldFail, filters: [{schemes: ["http"]}], url: "ftp://fake/ftp/url"}, + + // Multiple schemes: pass when at least one scheme matches. + {shouldPass, filters: [{schemes: ["https", "about"]}], url: "https://mozilla.org"}, + {shouldPass, filters: [{schemes: ["about", "https"]}], url: "https://mozilla.org"}, + {shouldFail, filters: [{schemes: ["about", "http"]}], url: "https://mozilla.org"}, + + // Port filter: standard (implicit) ports. + {shouldPass, filters: [{ports: [443]}], url: "https://mozilla.org"}, + {shouldPass, filters: [{ports: [80]}], url: "http://mozilla.org"}, + {shouldPass, filters: [{ports: [21]}], url: "ftp://ftp.mozilla.org"}, + + // Port filter: schemes without a default port. + {shouldFail, filters: [{ports: [-1]}], url: "about:blank"}, + {shouldFail, filters: [{ports: [-1]}], url: "data:,testDataURL"}, + + {shouldFail, filters: [{ports: [[1, 65535]]}], url: "about:blank"}, + {shouldFail, filters: [{ports: [[1, 65535]]}], url: "data:,testDataURL"}, + + // Host filters (hostEquals, hostContains, hostPrefix, hostSuffix): schemes with an host. + {shouldFail, filters: [{hostEquals: ""}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostEquals: null}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "https://mozilla.org"}, + {shouldFail, filters: [{hostEquals: "mozilla.com"}], url: "https://mozilla.org"}, + // NOTE: trying at least once another valid protocol. + {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "ftp://mozilla.org"}, + {shouldFail, filters: [{hostEquals: "mozilla.com"}], url: "ftp://mozilla.org"}, + {shouldPass, filters: [{hostEquals: "mozilla.org"}], url: "https://mozilla.org:8888"}, + + {shouldPass, filters: [{hostContains: "moz"}], url: "https://mozilla.org"}, + // NOTE: an implicit '.' char is inserted into the host. + {shouldPass, filters: [{hostContains: ".moz"}], url: "https://mozilla.org"}, + {shouldFail, filters: [{hostContains: "com"}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostContains: ""}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostContains: null}], url: "https://mozilla.org"}, + + {shouldPass, filters: [{hostPrefix: "moz"}], url: "https://mozilla.org"}, + {shouldFail, filters: [{hostPrefix: "org"}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostPrefix: ""}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostPrefix: null}], url: "https://mozilla.org"}, + + {shouldPass, filters: [{hostSuffix: ".org"}], url: "https://mozilla.org"}, + {shouldFail, filters: [{hostSuffix: "moz"}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostSuffix: ""}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostSuffix: null}], url: "https://mozilla.org"}, + {shouldPass, filters: [{hostSuffix: "lla.org"}], url: "https://mozilla.org:8888"}, + + // hostEquals: urls without an host. + // TODO: should we explicitly cover hostContains, hostPrefix, hostSuffix for + // these sub-cases? + {shouldFail, filters: [{hostEquals: "blank"}], url: "about:blank"}, + {shouldFail, filters: [{hostEquals: "blank"}], url: "about://blank"}, + {shouldFail, filters: [{hostEquals: "testDataURL"}], url: "data:,testDataURL"}, + {shouldPass, filters: [{hostEquals: ""}], url: "about:blank"}, + {shouldPass, filters: [{hostEquals: ""}], url: "about://blank"}, + {shouldPass, filters: [{hostEquals: ""}], url: "data:,testDataURL"}, + + // Path filters (pathEquals, pathContains, pathPrefix, pathSuffix). + {shouldFail, filters: [{pathEquals: ""}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathEquals: null}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathEquals: "/test/path"}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathEquals: "/wrong/path"}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathEquals: "/test/path"}], url: "https://mozilla.org:8888/test/path"}, + // NOTE: trying at least once another valid protocol + {shouldPass, filters: [{pathEquals: "/test/path"}], url: "ftp://mozilla.org/test/path"}, + {shouldFail, filters: [{pathEquals: "/wrong/path"}], url: "ftp://mozilla.org/test/path"}, + + {shouldPass, filters: [{pathContains: "st/"}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathContains: "/test"}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathContains: "org"}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathContains: ""}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathContains: null}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathContains: "param"}], url: "https://mozilla.org:8888/test/path?param=1"}, + {shouldFail, filters: [{pathContains: "ref"}], url: "https://mozilla.org:8888/test/path#ref"}, + {shouldPass, filters: [{pathContains: "st/pa"}], url: "https://mozilla.org:8888/test/path"}, + + {shouldPass, filters: [{pathPrefix: "/te"}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathPrefix: "org/"}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathPrefix: ""}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathPrefix: null}], url: "https://mozilla.org/test/path"}, + + {shouldPass, filters: [{pathSuffix: "/path"}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathSuffix: "th/"}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathSuffix: ""}], url: "https://mozilla.org/test/path"}, + {shouldPass, filters: [{pathSuffix: null}], url: "https://mozilla.org/test/path"}, + {shouldFail, filters: [{pathSuffix: "p=1"}], url: "https://mozilla.org:8888/test/path?p=1"}, + {shouldFail, filters: [{pathSuffix: "ref"}], url: "https://mozilla.org:8888/test/path#ref"}, + + // Query filters (queryEquals, queryContains, queryPrefix, querySuffix). + {shouldFail, filters: [{queryEquals: ""}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{queryEquals: null}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{queryEquals: "param=val"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryEquals: "?param=val"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryEquals: "/path?param=val"}], url: "https://mozilla.org/path?param=val"}, + + // NOTE: about scheme urls cannot be matched by query. + {shouldFail, filters: [{queryEquals: "param=val"}], url: "about:blank?param=val"}, + {shouldFail, filters: [{queryEquals: "param"}], url: "ftp://mozilla.org?param=val"}, + + {shouldPass, filters: [{queryContains: "ram"}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{queryContains: "=val"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryContains: "?param"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryContains: "path"}], url: "https://mozilla.org/path/?p=v#ref"}, + {shouldPass, filters: [{queryContains: ""}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{queryContains: null}], url: "https://mozilla.org/?param=val"}, + + {shouldPass, filters: [{queryPrefix: "param"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryPrefix: "p="}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{queryPrefix: "path"}], url: "https://mozilla.org/path?param=val"}, + {shouldPass, filters: [{queryPrefix: ""}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{queryPrefix: null}], url: "https://mozilla.org/?param=val"}, + + {shouldPass, filters: [{querySuffix: "=val"}], url: "https://mozilla.org/?param=val"}, + {shouldFail, filters: [{querySuffix: "=wrong"}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{querySuffix: ""}], url: "https://mozilla.org/?param=val"}, + {shouldPass, filters: [{querySuffix: null}], url: "https://mozilla.org/?param=val"}, + + // URL filters (urlEquals, urlContains, urlPrefix, urlSuffix). + {shouldFail, filters: [{urlEquals: ""}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlEquals: null}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlEquals: "https://mozilla.org/?p=v#ref"}], + url: "https://mozilla.org/?p=v#ref"}, + {shouldFail, filters: [{urlEquals: "https://mozilla.org/?p=v#ref2"}], + url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlEquals: "about:blank?p=v#ref"}], url: "about:blank?p=v#ref"}, + {shouldPass, filters: [{urlEquals: "ftp://mozilla.org?p=v#ref"}], + url: "ftp://mozilla.org?p=v#ref"}, + + {shouldPass, filters: [{urlContains: "org/?p"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlContains: "=v#ref"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldFail, filters: [{urlContains: "ftp"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlContains: ""}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlContains: null}], url: "https://mozilla.org/?p=v#ref"}, + + {shouldPass, filters: [{urlPrefix: "http"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldFail, filters: [{urlPrefix: "moz"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlPrefix: ""}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlPrefix: null}], url: "https://mozilla.org/?p=v#ref"}, + + {shouldPass, filters: [{urlSuffix: "#ref"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldFail, filters: [{urlSuffix: "=wrong"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlSuffix: ""}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlSuffix: null}], url: "https://mozilla.org/?p=v#ref"}, + + // More url filters: urlMatches. + {shouldPass, filters: [{urlMatches: ".*://mozilla"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlMatches: ".*://mozilla"}], url: "ftp://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlMatches: ".*://.*/\?p"}], url: "ftp://mozilla.org/?p=v#ref"}, + // NOTE: urlMatches should not match the url without the ref. + {shouldFail, filters: [{urlMatches: "v#ref$"}], url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{urlMatches: "^ftp"}], url: "ftp://mozilla.org/?p=v#ref"}, + + // More url filters: originAndPathMatches. + {shouldPass, filters: [{originAndPathMatches: ".*://mozilla"}], + url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{originAndPathMatches: ".*://mozilla"}], + url: "ftp://mozilla.org/?p=v#ref"}, + // NOTE: urlMatches should not match the url without the query and the ref. + {shouldFail, filters: [{originAndPathMatches: ".*://.*/\?p"}], + url: "ftp://mozilla.org/?p=v#ref"}, + {shouldFail, filters: [{originAndPathMatches: "v#ref$"}], + url: "https://mozilla.org/?p=v#ref"}, + {shouldPass, filters: [{originAndPathMatches: "^ftp"}], + url: "ftp://mozilla.org/?p=v#ref"}, + + // Filter with all criteria: all matches, none matches, some matches. + + // All matches. + {shouldPass, filters: [ + { + schemes: ["https", "http"], + ports: [443, 80], + hostEquals: "www.mozilla.org", + hostContains: ".moz", + hostPrefix: "www", + hostSuffix: "org", + pathEquals: "/sub/path", + pathContains: "b/p", + pathPrefix: "/sub", + pathSuffix: "/path", + queryEquals: "p=v", + queryContains: "1=", + queryPrefix: "p1", + querySuffix: "=v", + urlEquals: "https://www.mozilla.org/sub/path?p1=v#ref", + urlContains: "org/sub", + urlPrefix: "https://moz", + urlSuffix: "#ref", + urlMatches: "v#ref$", + originAndPathMatches: ".*://moz.*/" + }, + ], url: "https://www.mozilla.org/sub/path?p1=v#ref"}, + // None matches. + {shouldFail, filters: [ + { + schemes: ["http"], + ports: [80], + hostEquals: "mozilla.com", + hostContains: "www.moz", + hostPrefix: "www", + hostSuffix: "com", + pathEquals: "/wrong/path", + pathContains: "g/p", + pathPrefix: "/wrong", + pathSuffix: "/wrong", + queryEquals: "p2=v", + queryContains: "2=", + queryPrefix: "p2", + querySuffix: "=value", + urlEquals: "http://mozilla.com/sub/path?p1=v#ref", + urlContains: "com/sub", + urlPrefix: "http://moz", + urlSuffix: "#ref2", + urlMatches: "value#ref2$", + originAndPathMatches: ".*://moz.*com/" + }, + ], url: "https://mozilla.org/sub/path?p1=v#ref"}, + // Some matches + {shouldFail, filters: [ + { + schemes: ["https"], + ports: [80], + hostEquals: "mozilla.com", + hostContains: "www.moz", + hostPrefix: "www", + hostSuffix: "com", + pathEquals: "/wrong/path", + pathContains: "g/p", + pathPrefix: "/wrong", + pathSuffix: "/wrong", + queryEquals: "p2=v", + queryContains: "2=", + queryPrefix: "p2", + querySuffix: "=value", + urlEquals: "http://mozilla.com/sub/path?p1=v#ref", + urlContains: "com/sub", + urlPrefix: "http://moz", + urlSuffix: "#ref2", + urlMatches: "value#ref2$", + originAndPathMatches: ".*://moz.*com/" + }, + ], url: "https://mozilla.org/sub/path?p1=v#ref"}, + + // Filter with multiple filters: all matches, some matches, none matches. + + // All matches. + {shouldPass, filters: [ + {schemes: ["https", "http"]}, + {ports: [443, 80]}, + {hostEquals: "www.mozilla.org"}, + {hostContains: ".moz"}, + {hostPrefix: "www"}, + {hostSuffix: "org"}, + {pathEquals: "/sub/path"}, + {pathContains: "b/p"}, + {pathPrefix: "/sub"}, + {pathSuffix: "/path"}, + {queryEquals: "p=v"}, + {queryContains: "1="}, + {queryPrefix: "p1"}, + {querySuffix: "=v"}, + {urlEquals: "https://www.mozilla.org/sub/path?p1=v#ref"}, + {urlContains: "org/sub"}, + {urlPrefix: "https://moz"}, + {urlSuffix: "#ref"}, + {urlMatches: "v#ref$"}, + {originAndPathMatches: ".*://moz.*/"}, + ], url: "https://www.mozilla.org/sub/path?p1=v#ref"}, + + // None matches. + {shouldFail, filters: [ + {schemes: ["http"]}, + {ports: [80]}, + {hostEquals: "mozilla.com"}, + {hostContains: "www.moz"}, + {hostPrefix: "www"}, + {hostSuffix: "com"}, + {pathEquals: "/wrong/path"}, + {pathContains: "g/p"}, + {pathPrefix: "/wrong"}, + {pathSuffix: "/wrong"}, + {queryEquals: "p2=v"}, + {queryContains: "2="}, + {queryPrefix: "p2"}, + {querySuffix: "=value"}, + {urlEquals: "http://mozilla.com/sub/path?p1=v#ref"}, + {urlContains: "com/sub"}, + {urlPrefix: "http://moz"}, + {urlSuffix: "#ref2"}, + {urlMatches: "value#ref2$"}, + {originAndPathMatches: ".*://moz.*com/"}, + ], url: "https://mozilla.org/sub/path?p1=v#ref"}, + + // Some matches. + {shouldPass, filters: [ + {schemes: ["https"]}, + {ports: [80]}, + {hostEquals: "mozilla.com"}, + {hostContains: "www.moz"}, + {hostPrefix: "www"}, + {hostSuffix: "com"}, + {pathEquals: "/wrong/path"}, + {pathContains: "g/p"}, + {pathPrefix: "/wrong"}, + {pathSuffix: "/wrong"}, + {queryEquals: "p2=v"}, + {queryContains: "2="}, + {queryPrefix: "p2"}, + {querySuffix: "=value"}, + {urlEquals: "http://mozilla.com/sub/path?p1=v#ref"}, + {urlContains: "com/sub"}, + {urlPrefix: "http://moz"}, + {urlSuffix: "#ref2"}, + {urlMatches: "value#ref2$"}, + {originAndPathMatches: ".*://moz.*com/"}, + ], url: "https://mozilla.org/sub/path?p1=v#ref"}, + ]; + + // Run all the the testCases defined above. + for (let currentTest of testCases) { + let { + exceptionMessageContains, + url, filters, + } = currentTest; + + if (currentTest.shouldThrow) { + expectThrow({url, filters, exceptionMessageContains}) + } else if (currentTest.shouldFail) { + expectFail({url, filters}); + } else { + expectPass({url, filters}); + } + } +}); diff --git a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js new file mode 100644 index 000000000..8cdb63550 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js @@ -0,0 +1,378 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// See also browser/base/content/test/newtab/. + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +Cu.import("resource://gre/modules/NewTabUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced"; + +function run_test() { + Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, true); + run_next_test(); +} + +add_task(function* validCacheMidPopulation() { + let expectedLinks = makeLinks(0, 3, 1); + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + let promise = new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + + // isTopSiteGivenProvider() and getProviderLinks() should still return results + // even when cache is empty or being populated. + do_check_false(NewTabUtils.isTopSiteGivenProvider("example1.com", provider)); + do_check_links(NewTabUtils.getProviderLinks(provider), []); + + yield promise; + + // Once the cache is populated, we get the expected results + do_check_true(NewTabUtils.isTopSiteGivenProvider("example1.com", provider)); + do_check_links(NewTabUtils.getProviderLinks(provider), expectedLinks); + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function* notifyLinkDelete() { + let expectedLinks = makeLinks(0, 3, 1); + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Remove a link. + let removedLink = expectedLinks[2]; + provider.notifyLinkChanged(removedLink, 2, true); + let links = NewTabUtils.links._providers.get(provider); + + // Check that sortedLinks is correctly updated. + do_check_links(NewTabUtils.links.getLinks(), expectedLinks.slice(0, 2)); + + // Check that linkMap is accurately updated. + do_check_eq(links.linkMap.size, 2); + do_check_true(links.linkMap.get(expectedLinks[0].url)); + do_check_true(links.linkMap.get(expectedLinks[1].url)); + do_check_false(links.linkMap.get(removedLink.url)); + + // Check that siteMap is correctly updated. + do_check_eq(links.siteMap.size, 2); + do_check_true(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[0].url))); + do_check_true(links.siteMap.has(NewTabUtils.extractSite(expectedLinks[1].url))); + do_check_false(links.siteMap.has(NewTabUtils.extractSite(removedLink.url))); + + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function* populatePromise() { + let count = 0; + let expectedLinks = makeLinks(0, 10, 2); + + let getLinksFcn = Task.async(function* (callback) { + // Should not be calling getLinksFcn twice + count++; + do_check_eq(count, 1); + yield Promise.resolve(); + callback(expectedLinks); + }); + + let provider = new TestProvider(getLinksFcn); + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + + NewTabUtils.links.populateProviderCache(provider, () => {}); + NewTabUtils.links.populateProviderCache(provider, () => { + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + NewTabUtils.links.removeProvider(provider); + }); +}); + +add_task(function* isTopSiteGivenProvider() { + let expectedLinks = makeLinks(0, 10, 2); + + // The lowest 2 frecencies have the same base domain. + expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test"; + + let provider = new TestProvider(done => done(expectedLinks)); + provider.maxNumLinks = expectedLinks.length; + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example1.com", provider), false); + + // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3 + let newLink = makeLink(3); + provider.notifyLinkChanged(newLink); + + // There is still a frecent url with example2 domain, so it's still frecent. + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example3.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true); + + // Push out frecency 3 + newLink = makeLink(5); + provider.notifyLinkChanged(newLink); + + // Push out frecency 4 + newLink = makeLink(9); + provider.notifyLinkChanged(newLink); + + // Our count reached 0 for the example2.com domain so it's no longer a frecent site. + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example5.com", provider), true); + do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), false); + + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function* multipleProviders() { + // Make each provider generate NewTabUtils.links.maxNumLinks links to check + // that no more than maxNumLinks are actually returned in the merged list. + let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2); + let evenProvider = new TestProvider(done => done(evenLinks)); + let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2); + let oddProvider = new TestProvider(done => done(oddLinks)); + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(evenProvider); + NewTabUtils.links.addProvider(oddProvider); + + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + + let links = NewTabUtils.links.getLinks(); + let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks, + 2 * NewTabUtils.links.maxNumLinks, + 1); + do_check_eq(links.length, NewTabUtils.links.maxNumLinks); + do_check_links(links, expectedLinks); + + NewTabUtils.links.removeProvider(evenProvider); + NewTabUtils.links.removeProvider(oddProvider); +}); + +add_task(function* changeLinks() { + let expectedLinks = makeLinks(0, 20, 2); + let provider = new TestProvider(done => done(expectedLinks)); + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a new link. + let newLink = makeLink(19); + expectedLinks.splice(1, 0, newLink); + provider.notifyLinkChanged(newLink); + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a link that's changed sort criteria. + newLink.frecency = 17; + expectedLinks.splice(1, 1); + expectedLinks.splice(2, 0, newLink); + provider.notifyLinkChanged({ + url: newLink.url, + frecency: 17, + }); + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a link that's changed title. + newLink.title = "My frecency is now 17"; + provider.notifyLinkChanged({ + url: newLink.url, + title: newLink.title, + }); + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Notify of a new link again, but this time make it overflow maxNumLinks. + provider.maxNumLinks = expectedLinks.length; + newLink = makeLink(21); + expectedLinks.unshift(newLink); + expectedLinks.pop(); + do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check. + provider.notifyLinkChanged(newLink); + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + // Notify of many links changed. + expectedLinks = makeLinks(0, 3, 1); + provider.notifyManyLinksChanged(); + + // Since _populateProviderCache() is async, we must wait until the provider's + // populate promise has been resolved. + yield NewTabUtils.links._providers.get(provider).populatePromise; + + // NewTabUtils.links will now repopulate its cache + do_check_links(NewTabUtils.links.getLinks(), expectedLinks); + + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function* oneProviderAlreadyCached() { + let links1 = makeLinks(0, 10, 1); + let provider1 = new TestProvider(done => done(links1)); + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider1); + + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + do_check_links(NewTabUtils.links.getLinks(), links1); + + let links2 = makeLinks(10, 20, 1); + let provider2 = new TestProvider(done => done(links2)); + NewTabUtils.links.addProvider(provider2); + + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1)); + + NewTabUtils.links.removeProvider(provider1); + NewTabUtils.links.removeProvider(provider2); +}); + +add_task(function* newLowRankedLink() { + // Init a provider with 10 links and make its maximum number also 10. + let links = makeLinks(0, 10, 1); + let provider = new TestProvider(done => done(links)); + provider.maxNumLinks = links.length; + + NewTabUtils.initWithoutProviders(); + NewTabUtils.links.addProvider(provider); + + yield new Promise(resolve => NewTabUtils.links.populateCache(resolve)); + do_check_links(NewTabUtils.links.getLinks(), links); + + // Notify of a new link that's low-ranked enough not to make the list. + let newLink = makeLink(0); + provider.notifyLinkChanged(newLink); + do_check_links(NewTabUtils.links.getLinks(), links); + + // Notify about the new link's title change. + provider.notifyLinkChanged({ + url: newLink.url, + title: "a new title", + }); + do_check_links(NewTabUtils.links.getLinks(), links); + + NewTabUtils.links.removeProvider(provider); +}); + +add_task(function* extractSite() { + // All these should extract to the same site + [ "mozilla.org", + "m.mozilla.org", + "mobile.mozilla.org", + "www.mozilla.org", + "www3.mozilla.org", + ].forEach(host => { + let url = "http://" + host; + do_check_eq(NewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host); + }); + + // All these should extract to the same subdomain + [ "bugzilla.mozilla.org", + "www.bugzilla.mozilla.org", + ].forEach(host => { + let url = "http://" + host; + do_check_eq(NewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host); + }); + + // All these should not extract to the same site + [ "bugzilla.mozilla.org", + "bug123.bugzilla.mozilla.org", + "too.many.levels.bugzilla.mozilla.org", + "m2.mozilla.org", + "mobile30.mozilla.org", + "ww.mozilla.org", + "ww2.mozilla.org", + "wwwww.mozilla.org", + "wwwww50.mozilla.org", + "wwws.mozilla.org", + "secure.mozilla.org", + "secure10.mozilla.org", + "many.levels.deep.mozilla.org", + "just.check.in", + "192.168.0.1", + "localhost", + ].forEach(host => { + let url = "http://" + host; + do_check_neq(NewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host); + }); + + // All these should not extract to the same site + [ "about:blank", + "file:///Users/user/file", + "chrome://browser/something", + "ftp://ftp.mozilla.org/", + ].forEach(url => { + do_check_neq(NewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url); + }); +}); + +function TestProvider(getLinksFn) { + this.getLinks = getLinksFn; + this._observers = new Set(); +} + +TestProvider.prototype = { + addObserver: function (observer) { + this._observers.add(observer); + }, + notifyLinkChanged: function (link, index=-1, deleted=false) { + this._notifyObservers("onLinkChanged", link, index, deleted); + }, + notifyManyLinksChanged: function () { + this._notifyObservers("onManyLinksChanged"); + }, + _notifyObservers: function () { + let observerMethodName = arguments[0]; + let args = Array.prototype.slice.call(arguments, 1); + args.unshift(this); + for (let obs of this._observers) { + if (obs[observerMethodName]) + obs[observerMethodName].apply(NewTabUtils.links, args); + } + }, +}; + +function do_check_links(actualLinks, expectedLinks) { + do_check_true(Array.isArray(actualLinks)); + do_check_eq(actualLinks.length, expectedLinks.length); + for (let i = 0; i < expectedLinks.length; i++) { + let expected = expectedLinks[i]; + let actual = actualLinks[i]; + do_check_eq(actual.url, expected.url); + do_check_eq(actual.title, expected.title); + do_check_eq(actual.frecency, expected.frecency); + do_check_eq(actual.lastVisitDate, expected.lastVisitDate); + } +} + +function makeLinks(frecRangeStart, frecRangeEnd, step) { + let links = []; + // Remember, links are ordered by frecency descending. + for (let i = frecRangeEnd; i > frecRangeStart; i -= step) { + links.push(makeLink(i)); + } + return links; +} + +function makeLink(frecency) { + return { + url: "http://example" + frecency + ".com/", + title: "My frecency is " + frecency, + frecency: frecency, + lastVisitDate: 0, + }; +} diff --git a/toolkit/modules/tests/xpcshell/test_ObjectUtils.js b/toolkit/modules/tests/xpcshell/test_ObjectUtils.js new file mode 100644 index 000000000..9aef3e907 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils.js @@ -0,0 +1,96 @@ +Components.utils.import("resource://gre/modules/ObjectUtils.jsm"); + +function run_test() { + run_next_test(); +} + +add_task(function* test_deepEqual() { + let deepEqual = ObjectUtils.deepEqual.bind(ObjectUtils); + // CommonJS 7.2 + Assert.ok(deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14)), "deepEqual date"); + Assert.ok(deepEqual(new Date(NaN), new Date(NaN)), "deepEqual invalid dates"); + + Assert.ok(!deepEqual(new Date(), new Date(2000, 3, 14)), "deepEqual date"); + + // 7.3 + Assert.ok(deepEqual(/a/, /a/)); + Assert.ok(deepEqual(/a/g, /a/g)); + Assert.ok(deepEqual(/a/i, /a/i)); + Assert.ok(deepEqual(/a/m, /a/m)); + Assert.ok(deepEqual(/a/igm, /a/igm)); + Assert.ok(!deepEqual(/ab/, /a/)); + Assert.ok(!deepEqual(/a/g, /a/)); + Assert.ok(!deepEqual(/a/i, /a/)); + Assert.ok(!deepEqual(/a/m, /a/)); + Assert.ok(!deepEqual(/a/igm, /a/im)); + + let re1 = /a/; + re1.lastIndex = 3; + Assert.ok(!deepEqual(re1, /a/)); + + // 7.4 + Assert.ok(deepEqual(4, "4"), "deepEqual == check"); + Assert.ok(deepEqual(true, 1), "deepEqual == check"); + Assert.ok(!deepEqual(4, "5"), "deepEqual == check"); + + // 7.5 + // having the same number of owned properties && the same set of keys + Assert.ok(deepEqual({a: 4}, {a: 4})); + Assert.ok(deepEqual({a: 4, b: "2"}, {a: 4, b: "2"})); + Assert.ok(deepEqual([4], ["4"])); + Assert.ok(!deepEqual({a: 4}, {a: 4, b: true})); + Assert.ok(deepEqual(["a"], {0: "a"})); + + let a1 = [1, 2, 3]; + let a2 = [1, 2, 3]; + a1.a = "test"; + a1.b = true; + a2.b = true; + a2.a = "test"; + Assert.ok(!deepEqual(Object.keys(a1), Object.keys(a2))); + Assert.ok(deepEqual(a1, a2)); + + let nbRoot = { + toString: function() { return this.first + " " + this.last; } + }; + + function nameBuilder(first, last) { + this.first = first; + this.last = last; + return this; + } + nameBuilder.prototype = nbRoot; + + function nameBuilder2(first, last) { + this.first = first; + this.last = last; + return this; + } + nameBuilder2.prototype = nbRoot; + + let nb1 = new nameBuilder("Ryan", "Dahl"); + let nb2 = new nameBuilder2("Ryan", "Dahl"); + + Assert.ok(deepEqual(nb1, nb2)); + + nameBuilder2.prototype = Object; + nb2 = new nameBuilder2("Ryan", "Dahl"); + Assert.ok(!deepEqual(nb1, nb2)); + + // String literal + object + Assert.ok(!deepEqual("a", {})); + + // Make sure deepEqual doesn't loop forever on circular refs + + let b = {}; + b.b = b; + + let c = {}; + c.b = c; + + try { + Assert.ok(!deepEqual(b, c)); + } catch (e) { + Assert.ok(true, "Didn't recurse infinitely."); + } +}); diff --git a/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js new file mode 100644 index 000000000..44572e600 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_ObjectUtils_strict.js @@ -0,0 +1,29 @@ +"use strict"; + +var {ObjectUtils} = Components.utils.import("resource://gre/modules/ObjectUtils.jsm", {}); +var {PromiseTestUtils} = Components.utils.import("resource://testing-common/PromiseTestUtils.jsm", {}); + +add_task(function* test_strict() { + let loose = { a: 1 }; + let strict = ObjectUtils.strict(loose); + + loose.a; // Should not throw. + loose.b || undefined; // Should not throw. + + strict.a; // Should not throw. + PromiseTestUtils.expectUncaughtRejection(/No such property: "b"/); + Assert.throws(() => strict.b, /No such property: "b"/); + "b" in strict; // Should not throw. + strict.b = 2; + strict.b; // Should not throw. + + PromiseTestUtils.expectUncaughtRejection(/No such property: "c"/); + Assert.throws(() => strict.c, /No such property: "c"/); + "c" in strict; // Should not throw. + loose.c = 3; + strict.c; // Should not throw. +}); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js new file mode 100644 index 000000000..3982ce015 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_PermissionsUtils.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that PerrmissionsUtils.jsm works as expected, including: +// * PermissionsUtils.importfromPrefs() +// .[whitelist|blacklist].add preferences are emptied when +// converted into permissions on startup. + + +const PREF_ROOT = "testpermissions."; +const TEST_PERM = "test-permission"; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/PermissionsUtils.jsm"); + +function run_test() { + test_importfromPrefs(); +} + + +function test_importfromPrefs() { + // Create own preferences to test + Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.EMPTY", ""); + Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.EMPTY2", ","); + Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST", "http://whitelist.example.com"); + Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST2", "https://whitelist2-1.example.com,http://whitelist2-2.example.com:8080,about:home"); + Services.prefs.setCharPref(PREF_ROOT + "whitelist.add.TEST3", "whitelist3-1.example.com,about:config"); // legacy style - host only + Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.EMPTY", ""); + Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST", "http://blacklist.example.com,"); + Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST2", ",https://blacklist2-1.example.com,http://blacklist2-2.example.com:8080,about:mozilla"); + Services.prefs.setCharPref(PREF_ROOT + "blacklist.add.TEST3", "blacklist3-1.example.com,about:preferences"); // legacy style - host only + + // Check they are unknown in the permission manager prior to importing. + let whitelisted = ["http://whitelist.example.com", + "https://whitelist2-1.example.com", + "http://whitelist2-2.example.com:8080", + "http://whitelist3-1.example.com", + "https://whitelist3-1.example.com", + "about:config", + "about:home"]; + let blacklisted = ["http://blacklist.example.com", + "https://blacklist2-1.example.com", + "http://blacklist2-2.example.com:8080", + "http://blacklist3-1.example.com", + "https://blacklist3-1.example.com", + "about:preferences", + "about:mozilla"]; + let untouched = ["https://whitelist.example.com", + "https://blacklist.example.com", + "http://whitelist2-1.example.com", + "http://blacklist2-1.example.com", + "https://whitelist2-2.example.com:8080", + "https://blacklist2-2.example.com:8080"]; + let unknown = whitelisted.concat(blacklisted).concat(untouched); + for (let url of unknown) { + let uri = Services.io.newURI(url, null, null); + do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.UNKNOWN_ACTION); + } + + // Import them + PermissionsUtils.importFromPrefs(PREF_ROOT, TEST_PERM); + + // Get list of preferences to check + let preferences = Services.prefs.getChildList(PREF_ROOT, {}); + + // Check preferences were emptied + for (let pref of preferences) { + do_check_eq(Services.prefs.getCharPref(pref), ""); + } + + // Check they were imported into the permissions manager + for (let url of whitelisted) { + let uri = Services.io.newURI(url, null, null); + do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.ALLOW_ACTION); + } + for (let url of blacklisted) { + let uri = Services.io.newURI(url, null, null); + do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.DENY_ACTION); + } + for (let url of untouched) { + let uri = Services.io.newURI(url, null, null); + do_check_eq(Services.perms.testPermission(uri, TEST_PERM), Services.perms.UNKNOWN_ACTION); + } +} diff --git a/toolkit/modules/tests/xpcshell/test_Preferences.js b/toolkit/modules/tests/xpcshell/test_Preferences.js new file mode 100644 index 000000000..ef430909f --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Preferences.js @@ -0,0 +1,378 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu, manager: Cm} = Components; + +Cu.import("resource://gre/modules/Preferences.jsm"); + +function run_test() { + run_next_test(); +} + +add_test(function test_set_get_pref() { + Preferences.set("test_set_get_pref.integer", 1); + do_check_eq(Preferences.get("test_set_get_pref.integer"), 1); + + Preferences.set("test_set_get_pref.string", "foo"); + do_check_eq(Preferences.get("test_set_get_pref.string"), "foo"); + + Preferences.set("test_set_get_pref.boolean", true); + do_check_eq(Preferences.get("test_set_get_pref.boolean"), true); + + // Clean up. + Preferences.resetBranch("test_set_get_pref."); + + run_next_test(); +}); + +add_test(function test_set_get_branch_pref() { + let prefs = new Preferences("test_set_get_branch_pref."); + + prefs.set("something", 1); + do_check_eq(prefs.get("something"), 1); + do_check_false(Preferences.has("something")); + + // Clean up. + prefs.reset("something"); + + run_next_test(); +}); + +add_test(function test_set_get_multiple_prefs() { + Preferences.set({ "test_set_get_multiple_prefs.integer": 1, + "test_set_get_multiple_prefs.string": "foo", + "test_set_get_multiple_prefs.boolean": true }); + + let [i, s, b] = Preferences.get(["test_set_get_multiple_prefs.integer", + "test_set_get_multiple_prefs.string", + "test_set_get_multiple_prefs.boolean"]); + + do_check_eq(i, 1); + do_check_eq(s, "foo"); + do_check_eq(b, true); + + // Clean up. + Preferences.resetBranch("test_set_get_multiple_prefs."); + + run_next_test(); +}); + +add_test(function test_get_multiple_prefs_with_default_value() { + Preferences.set({ "test_get_multiple_prefs_with_default_value.a": 1, + "test_get_multiple_prefs_with_default_value.b": 2 }); + + let [a, b, c] = Preferences.get(["test_get_multiple_prefs_with_default_value.a", + "test_get_multiple_prefs_with_default_value.b", + "test_get_multiple_prefs_with_default_value.c"], + 0); + + do_check_eq(a, 1); + do_check_eq(b, 2); + do_check_eq(c, 0); + + // Clean up. + Preferences.resetBranch("test_get_multiple_prefs_with_default_value."); + + run_next_test(); +}); + +add_test(function test_set_get_unicode_pref() { + Preferences.set("test_set_get_unicode_pref", String.fromCharCode(960)); + do_check_eq(Preferences.get("test_set_get_unicode_pref"), String.fromCharCode(960)); + + // Clean up. + Preferences.reset("test_set_get_unicode_pref"); + + run_next_test(); +}); + +add_test(function test_set_null_pref() { + try { + Preferences.set("test_set_null_pref", null); + // We expect this to throw, so the test is designed to fail if it doesn't. + do_check_true(false); + } + catch (ex) {} + + run_next_test(); +}); + +add_test(function test_set_undefined_pref() { + try { + Preferences.set("test_set_undefined_pref"); + // We expect this to throw, so the test is designed to fail if it doesn't. + do_check_true(false); + } + catch (ex) {} + + run_next_test(); +}); + +add_test(function test_set_unsupported_pref() { + try { + Preferences.set("test_set_unsupported_pref", new Array()); + // We expect this to throw, so the test is designed to fail if it doesn't. + do_check_true(false); + } + catch (ex) {} + + run_next_test(); +}); + +// Make sure that we can get a string pref that we didn't set ourselves +// (i.e. that the way we get a string pref using getComplexValue doesn't +// hork us getting a string pref that wasn't set using setComplexValue). +add_test(function test_get_string_pref() { + let svc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(""); + svc.setCharPref("test_get_string_pref", "a normal string"); + do_check_eq(Preferences.get("test_get_string_pref"), "a normal string"); + + // Clean up. + Preferences.reset("test_get_string_pref"); + + run_next_test(); +}); + +add_test(function test_get_localized_string_pref() { + let svc = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService). + getBranch(""); + let prefName = "test_get_localized_string_pref"; + let localizedString = Cc["@mozilla.org/pref-localizedstring;1"] + .createInstance(Ci.nsIPrefLocalizedString); + localizedString.data = "a localized string"; + svc.setComplexValue(prefName, Ci.nsIPrefLocalizedString, localizedString); + do_check_eq(Preferences.get(prefName, null, Ci.nsIPrefLocalizedString), + "a localized string"); + + // Clean up. + Preferences.reset(prefName); + + run_next_test(); +}); + +add_test(function test_set_get_number_pref() { + Preferences.set("test_set_get_number_pref", 5); + do_check_eq(Preferences.get("test_set_get_number_pref"), 5); + + // Non-integer values get converted to integers. + Preferences.set("test_set_get_number_pref", 3.14159); + do_check_eq(Preferences.get("test_set_get_number_pref"), 3); + + // Values outside the range -(2^31-1) to 2^31-1 overflow. + try { + Preferences.set("test_set_get_number_pref", Math.pow(2, 31)); + // We expect this to throw, so the test is designed to fail if it doesn't. + do_check_true(false); + } + catch (ex) {} + + // Clean up. + Preferences.reset("test_set_get_number_pref"); + + run_next_test(); +}); + +add_test(function test_reset_pref() { + Preferences.set("test_reset_pref", 1); + Preferences.reset("test_reset_pref"); + do_check_eq(Preferences.get("test_reset_pref"), undefined); + + run_next_test(); +}); + +add_test(function test_reset_pref_branch() { + Preferences.set("test_reset_pref_branch.foo", 1); + Preferences.set("test_reset_pref_branch.bar", 2); + Preferences.resetBranch("test_reset_pref_branch."); + do_check_eq(Preferences.get("test_reset_pref_branch.foo"), undefined); + do_check_eq(Preferences.get("test_reset_pref_branch.bar"), undefined); + + run_next_test(); +}); + +// Make sure the module doesn't throw an exception when asked to reset +// a nonexistent pref. +add_test(function test_reset_nonexistent_pref() { + Preferences.reset("test_reset_nonexistent_pref"); + + run_next_test(); +}); + +// Make sure the module doesn't throw an exception when asked to reset +// a nonexistent pref branch. +add_test(function test_reset_nonexistent_pref_branch() { + Preferences.resetBranch("test_reset_nonexistent_pref_branch."); + + run_next_test(); +}); + +add_test(function test_observe_prefs_function() { + let observed = false; + let observer = function() { observed = !observed }; + + Preferences.observe("test_observe_prefs_function", observer); + Preferences.set("test_observe_prefs_function", "something"); + do_check_true(observed); + + Preferences.ignore("test_observe_prefs_function", observer); + Preferences.set("test_observe_prefs_function", "something else"); + do_check_true(observed); + + // Clean up. + Preferences.reset("test_observe_prefs_function"); + + run_next_test(); +}); + +add_test(function test_observe_prefs_object() { + let observer = { + observed: false, + observe: function() { + this.observed = !this.observed; + } + }; + + Preferences.observe("test_observe_prefs_object", observer.observe, observer); + Preferences.set("test_observe_prefs_object", "something"); + do_check_true(observer.observed); + + Preferences.ignore("test_observe_prefs_object", observer.observe, observer); + Preferences.set("test_observe_prefs_object", "something else"); + do_check_true(observer.observed); + + // Clean up. + Preferences.reset("test_observe_prefs_object"); + + run_next_test(); +}); + +add_test(function test_observe_prefs_nsIObserver() { + let observer = { + observed: false, + observe: function(subject, topic, data) { + this.observed = !this.observed; + do_check_true(subject instanceof Ci.nsIPrefBranch); + do_check_eq(topic, "nsPref:changed"); + do_check_eq(data, "test_observe_prefs_nsIObserver"); + } + }; + + Preferences.observe("test_observe_prefs_nsIObserver", observer); + Preferences.set("test_observe_prefs_nsIObserver", "something"); + do_check_true(observer.observed); + + Preferences.ignore("test_observe_prefs_nsIObserver", observer); + Preferences.set("test_observe_prefs_nsIObserver", "something else"); + do_check_true(observer.observed); + + // Clean up. + Preferences.reset("test_observe_prefs_nsIObserver"); + + run_next_test(); +}); + +/* +add_test(function test_observe_exact_pref() { + let observed = false; + let observer = function() { observed = !observed }; + + Preferences.observe("test_observe_exact_pref", observer); + Preferences.set("test_observe_exact_pref.sub-pref", "something"); + do_check_false(observed); + + // Clean up. + Preferences.ignore("test_observe_exact_pref", observer); + Preferences.reset("test_observe_exact_pref.sub-pref"); + + run_next_test(); +}); +*/ + +add_test(function test_observe_value_of_set_pref() { + let observer = function(newVal) { do_check_eq(newVal, "something") }; + + Preferences.observe("test_observe_value_of_set_pref", observer); + Preferences.set("test_observe_value_of_set_pref", "something"); + + // Clean up. + Preferences.ignore("test_observe_value_of_set_pref", observer); + Preferences.reset("test_observe_value_of_set_pref"); + + run_next_test(); +}); + +add_test(function test_observe_value_of_reset_pref() { + let observer = function(newVal) { do_check_true(typeof newVal == "undefined") }; + + Preferences.set("test_observe_value_of_reset_pref", "something"); + Preferences.observe("test_observe_value_of_reset_pref", observer); + Preferences.reset("test_observe_value_of_reset_pref"); + + // Clean up. + Preferences.ignore("test_observe_value_of_reset_pref", observer); + + run_next_test(); +}); + +add_test(function test_has_pref() { + do_check_false(Preferences.has("test_has_pref")); + Preferences.set("test_has_pref", "foo"); + do_check_true(Preferences.has("test_has_pref")); + + Preferences.set("test_has_pref.foo", "foo"); + Preferences.set("test_has_pref.bar", "bar"); + let [hasFoo, hasBar, hasBaz] = Preferences.has(["test_has_pref.foo", + "test_has_pref.bar", + "test_has_pref.baz"]); + do_check_true(hasFoo); + do_check_true(hasBar); + do_check_false(hasBaz); + + // Clean up. + Preferences.resetBranch("test_has_pref"); + + run_next_test(); +}); + +add_test(function test_isSet_pref() { + // Use a pref that we know has a default value but no user-set value. + // This feels dangerous; perhaps we should create some other default prefs + // that we can use for testing. + do_check_false(Preferences.isSet("toolkit.defaultChromeURI")); + Preferences.set("toolkit.defaultChromeURI", "foo"); + do_check_true(Preferences.isSet("toolkit.defaultChromeURI")); + + // Clean up. + Preferences.reset("toolkit.defaultChromeURI"); + + run_next_test(); +}); + +/* +add_test(function test_lock_prefs() { + // Use a pref that we know has a default value. + // This feels dangerous; perhaps we should create some other default prefs + // that we can use for testing. + do_check_false(Preferences.locked("toolkit.defaultChromeURI")); + Preferences.lock("toolkit.defaultChromeURI"); + do_check_true(Preferences.locked("toolkit.defaultChromeURI")); + Preferences.unlock("toolkit.defaultChromeURI"); + do_check_false(Preferences.locked("toolkit.defaultChromeURI")); + + let val = Preferences.get("toolkit.defaultChromeURI"); + Preferences.set("toolkit.defaultChromeURI", "test_lock_prefs"); + do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs"); + Preferences.lock("toolkit.defaultChromeURI"); + do_check_eq(Preferences.get("toolkit.defaultChromeURI"), val); + Preferences.unlock("toolkit.defaultChromeURI"); + do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs"); + + // Clean up. + Preferences.reset("toolkit.defaultChromeURI"); + + run_next_test(); +}); +*/ diff --git a/toolkit/modules/tests/xpcshell/test_Promise.js b/toolkit/modules/tests/xpcshell/test_Promise.js new file mode 100644 index 000000000..6c7220692 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Promise.js @@ -0,0 +1,1105 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +Components.utils.import("resource://gre/modules/Promise.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/Task.jsm"); +Components.utils.import("resource://testing-common/PromiseTestUtils.jsm"); + +// Prevent test failures due to the unhandled rejections in this test file. +PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest(); + +// Test runner + +var run_promise_tests = function run_promise_tests(tests, cb) { + let loop = function loop(index) { + if (index >= tests.length) { + if (cb) { + cb.call(); + } + return; + } + do_print("Launching test " + (index + 1) + "/" + tests.length); + let test = tests[index]; + // Execute from an empty stack + let next = function next() { + do_print("Test " + (index + 1) + "/" + tests.length + " complete"); + do_execute_soon(function() { + loop(index + 1); + }); + }; + let result = test(); + result.then(next, next); + }; + return loop(0); +}; + +var make_promise_test = function(test) { + return function runtest() { + do_print("Test starting: " + test.name); + try { + let result = test(); + if (result && "promise" in result) { + result = result.promise; + } + if (!result || !("then" in result)) { + let exn; + try { + do_throw("Test " + test.name + " did not return a promise: " + result); + } catch (x) { + exn = x; + } + return Promise.reject(exn); + } + // The test returns a promise + result = result.then( + // Test complete + function onResolve() { + do_print("Test complete: " + test.name); + }, + // The test failed with an unexpected error + function onReject(err) { + let detail; + if (err && typeof err == "object" && "stack" in err) { + detail = err.stack; + } else { + detail = "(no stack)"; + } + do_throw("Test " + test.name + " rejected with the following reason: " + + err + detail); + }); + return result; + } catch (x) { + // The test failed because of an error outside of a promise + do_throw("Error in body of test " + test.name + ": " + x + " at " + x.stack); + return Promise.reject(); + } + }; +}; + +// Tests + +var tests = []; + +// Utility function to observe an failures in a promise +// This function is useful if the promise itself is +// not returned. +var observe_failures = function observe_failures(promise) { + promise.catch(function onReject(reason) { + test.do_throw("Observed failure in test " + test + ": " + reason); + }); +}; + +// Test that all observers are notified +tests.push(make_promise_test( + function notification(test) { + // The size of the test + const SIZE = 10; + const RESULT = "this is an arbitrary value"; + + // Number of observers that yet need to be notified + let expected = SIZE; + + // |true| once an observer has been notified + let notified = []; + + // The promise observed + let source = Promise.defer(); + let result = Promise.defer(); + + let install_observer = function install_observer(i) { + observe_failures(source.promise.then( + function onSuccess(value) { + do_check_true(!notified[i], "Ensuring that observer is notified at most once"); + notified[i] = true; + + do_check_eq(value, RESULT, "Ensuring that the observed value is correct"); + if (--expected == 0) { + result.resolve(); + } + })); + }; + + // Install a number of observers before resolving + let i; + for (i = 0; i < SIZE/2; ++i) { + install_observer(i); + } + + source.resolve(RESULT); + + // Install remaining observers + for (;i < SIZE; ++i) { + install_observer(i); + } + + return result; + })); + +// Test that observers get the correct "this" value in strict mode. +tests.push( + make_promise_test(function handlers_this_value(test) { + return Promise.resolve().then( + function onResolve() { + // Since this file is in strict mode, the correct value is "undefined". + do_check_eq(this, undefined); + throw "reject"; + } + ).then( + null, + function onReject() { + // Since this file is in strict mode, the correct value is "undefined". + do_check_eq(this, undefined); + } + ); + })); + +// Test that observers registered on a pending promise are notified in order. +tests.push( + make_promise_test(function then_returns_before_callbacks(test) { + let deferred = Promise.defer(); + let promise = deferred.promise; + + let order = 0; + + promise.then( + function onResolve() { + do_check_eq(order, 0); + order++; + } + ); + + promise.then( + function onResolve() { + do_check_eq(order, 1); + order++; + } + ); + + let newPromise = promise.then( + function onResolve() { + do_check_eq(order, 2); + } + ); + + deferred.resolve(); + + // This test finishes after the last handler succeeds. + return newPromise; + })); + +// Test that observers registered on a resolved promise are notified in order. +tests.push( + make_promise_test(function then_returns_before_callbacks(test) { + let promise = Promise.resolve(); + + let order = 0; + + promise.then( + function onResolve() { + do_check_eq(order, 0); + order++; + } + ); + + promise.then( + function onResolve() { + do_check_eq(order, 1); + order++; + } + ); + + // This test finishes after the last handler succeeds. + return promise.then( + function onResolve() { + do_check_eq(order, 2); + } + ); + })); + +// Test that all observers are notified at most once, even if source +// is resolved/rejected several times +tests.push(make_promise_test( + function notification_once(test) { + // The size of the test + const SIZE = 10; + const RESULT = "this is an arbitrary value"; + + // Number of observers that yet need to be notified + let expected = SIZE; + + // |true| once an observer has been notified + let notified = []; + + // The promise observed + let observed = Promise.defer(); + let result = Promise.defer(); + + let install_observer = function install_observer(i) { + observe_failures(observed.promise.then( + function onSuccess(value) { + do_check_true(!notified[i], "Ensuring that observer is notified at most once"); + notified[i] = true; + + do_check_eq(value, RESULT, "Ensuring that the observed value is correct"); + if (--expected == 0) { + result.resolve(); + } + })); + }; + + // Install a number of observers before resolving + let i; + for (i = 0; i < SIZE/2; ++i) { + install_observer(i); + } + + observed.resolve(RESULT); + + // Install remaining observers + for (;i < SIZE; ++i) { + install_observer(i); + } + + // Resolve some more + for (i = 0; i < 10; ++i) { + observed.resolve(RESULT); + observed.reject(); + } + + return result; + })); + +// Test that throwing an exception from a onResolve listener +// does not prevent other observers from receiving the notification +// of success. +tests.push( + make_promise_test(function exceptions_do_not_stop_notifications(test) { + let source = Promise.defer(); + + let exception_thrown = false; + let exception_content = new Error("Boom!"); + + let observer_1 = source.promise.then( + function onResolve() { + exception_thrown = true; + throw exception_content; + }); + + let observer_2 = source.promise.then( + function onResolve() { + do_check_true(exception_thrown, "Second observer called after first observer has thrown"); + } + ); + + let result = observer_1.then( + function onResolve() { + do_throw("observer_1 should not have resolved"); + }, + function onReject(reason) { + do_check_true(reason == exception_content, "Obtained correct rejection"); + } + ); + + source.resolve(); + return result; + } +)); + +// Test that, once a promise is resolved, further resolve/reject +// are ignored. +tests.push( + make_promise_test(function subsequent_resolves_are_ignored(test) { + let deferred = Promise.defer(); + deferred.resolve(1); + deferred.resolve(2); + deferred.reject(3); + + let result = deferred.promise.then( + function onResolve(value) { + do_check_eq(value, 1, "Resolution chose the first value"); + }, + function onReject(reason) { + do_throw("Obtained a rejection while the promise was already resolved"); + } + ); + + return result; + })); + +// Test that, once a promise is rejected, further resolve/reject +// are ignored. +tests.push( + make_promise_test(function subsequent_rejects_are_ignored(test) { + let deferred = Promise.defer(); + deferred.reject(1); + deferred.reject(2); + deferred.resolve(3); + + let result = deferred.promise.then( + function onResolve() { + do_throw("Obtained a resolution while the promise was already rejected"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "Rejection chose the first value"); + } + ); + + return result; + })); + +// Test that returning normally from a rejection recovers from the error +// and that listeners are informed of a success. +tests.push( + make_promise_test(function recovery(test) { + let boom = new Error("Boom!"); + let deferred = Promise.defer(); + const RESULT = "An arbitrary value"; + + let promise = deferred.promise.then( + function onResolve() { + do_throw("A rejected promise should not resolve"); + }, + function onReject(reason) { + do_check_true(reason == boom, "Promise was rejected with the correct error"); + return RESULT; + } + ); + + promise = promise.then( + function onResolve(value) { + do_check_eq(value, RESULT, "Promise was recovered with the correct value"); + } + ); + + deferred.reject(boom); + return promise; + })); + +// Test that returning a resolved promise from a onReject causes a resolution +// (recovering from the error) and that returning a rejected promise +// from a onResolve listener causes a rejection (raising an error). +tests.push( + make_promise_test(function recovery_with_promise(test) { + let boom = new Error("Arbitrary error"); + let deferred = Promise.defer(); + const RESULT = "An arbitrary value"; + const boom2 = new Error("Another arbitrary error"); + + // return a resolved promise from a onReject listener + let promise = deferred.promise.then( + function onResolve() { + do_throw("A rejected promise should not resolve"); + }, + function onReject(reason) { + do_check_true(reason == boom, "Promise was rejected with the correct error"); + return Promise.resolve(RESULT); + } + ); + + // return a rejected promise from a onResolve listener + promise = promise.then( + function onResolve(value) { + do_check_eq(value, RESULT, "Promise was recovered with the correct value"); + return Promise.reject(boom2); + } + ); + + promise = promise.catch( + function onReject(reason) { + do_check_eq(reason, boom2, "Rejection was propagated with the correct " + + "reason, through a promise"); + } + ); + + deferred.reject(boom); + return promise; + })); + +// Test that we can resolve with promises of promises +tests.push( + make_promise_test(function test_propagation(test) { + const RESULT = "Yet another arbitrary value"; + let d1 = Promise.defer(); + let d2 = Promise.defer(); + let d3 = Promise.defer(); + + d3.resolve(d2.promise); + d2.resolve(d1.promise); + d1.resolve(RESULT); + + return d3.promise.then( + function onSuccess(value) { + do_check_eq(value, RESULT, "Resolution with a promise eventually yielded " + + " the correct result"); + } + ); + })); + +// Test sequences of |then| and |catch| +tests.push( + make_promise_test(function test_chaining(test) { + let error_1 = new Error("Error 1"); + let error_2 = new Error("Error 2"); + let result_1 = "First result"; + let result_2 = "Second result"; + let result_3 = "Third result"; + + let source = Promise.defer(); + + let promise = source.promise.then().then(); + + source.resolve(result_1); + + // Check that result_1 is correctly propagated + promise = promise.then( + function onSuccess(result) { + do_check_eq(result, result_1, "Result was propagated correctly through " + + " several applications of |then|"); + return result_2; + } + ); + + // Check that returning from the promise produces a resolution + promise = promise.catch( + function onReject() { + do_throw("Incorrect rejection"); + } + ); + + // ... and that the check did not alter the value + promise = promise.then( + function onResolve(value) { + do_check_eq(value, result_2, "Result was propagated correctly once again"); + } + ); + + // Now the same kind of tests for rejections + promise = promise.then( + function onResolve() { + throw error_1; + } + ); + + promise = promise.then( + function onResolve() { + do_throw("Incorrect resolution: the exception should have caused a rejection"); + } + ); + + promise = promise.catch( + function onReject(reason) { + do_check_true(reason == error_1, "Reason was propagated correctly"); + throw error_2; + } + ); + + promise = promise.catch( + function onReject(reason) { + do_check_true(reason == error_2, "Throwing an error altered the reason " + + "as expected"); + return result_3; + } + ); + + promise = promise.then( + function onResolve(result) { + do_check_eq(result, result_3, "Error was correctly recovered"); + } + ); + + return promise; + })); + +// Test that resolving with a rejected promise actually rejects +tests.push( + make_promise_test(function resolve_to_rejected(test) { + let source = Promise.defer(); + let error = new Error("Boom"); + + let promise = source.promise.then( + function onResolve() { + do_throw("Incorrect call to onResolve listener"); + }, + function onReject(reason) { + do_check_eq(reason, error, "Rejection lead to the expected reason"); + } + ); + + source.resolve(Promise.reject(error)); + + return promise; + })); + +// Test that Promise.resolve resolves as expected +tests.push( + make_promise_test(function test_resolve(test) { + const RESULT = "arbitrary value"; + let p1 = Promise.resolve(RESULT); + let p2 = Promise.resolve(p1); + do_check_eq(p1, p2, "Promise.resolve used on a promise just returns the promise"); + + return p1.then( + function onResolve(result) { + do_check_eq(result, RESULT, "Promise.resolve propagated the correct result"); + } + ); + })); + +// Test that Promise.resolve throws when its argument is an async function. +tests.push( + make_promise_test(function test_promise_resolve_throws_with_async_function(test) { + Assert.throws(() => Promise.resolve(Task.async(function* () {})), + /Cannot resolve a promise with an async function/); + return Promise.resolve(); + })); + +// Test that the code after "then" is always executed before the callbacks +tests.push( + make_promise_test(function then_returns_before_callbacks(test) { + let promise = Promise.resolve(); + + let thenExecuted = false; + + promise = promise.then( + function onResolve() { + thenExecuted = true; + } + ); + + do_check_false(thenExecuted); + + return promise; + })); + +// Test that chaining promises does not generate long stack traces +tests.push( + make_promise_test(function chaining_short_stack(test) { + let source = Promise.defer(); + let promise = source.promise; + + const NUM_ITERATIONS = 100; + + for (let i = 0; i < NUM_ITERATIONS; i++) { + promise = promise.then( + function onResolve(result) { + return result + "."; + } + ); + } + + promise = promise.then( + function onResolve(result) { + // Check that the execution went as expected. + let expectedString = new Array(1 + NUM_ITERATIONS).join("."); + do_check_true(result == expectedString); + + // Check that we didn't generate one or more stack frames per iteration. + let stackFrameCount = 0; + let stackFrame = Components.stack; + while (stackFrame) { + stackFrameCount++; + stackFrame = stackFrame.caller; + } + + do_check_true(stackFrameCount < NUM_ITERATIONS); + } + ); + + source.resolve(""); + + return promise; + })); + +// Test that the values of the promise return by Promise.all() are kept in the +// given order even if the given promises are resolved in arbitrary order +tests.push( + make_promise_test(function all_resolve(test) { + let d1 = Promise.defer(); + let d2 = Promise.defer(); + let d3 = Promise.defer(); + + d3.resolve(4); + d2.resolve(2); + do_execute_soon(() => d1.resolve(1)); + + let promises = [d1.promise, d2.promise, 3, d3.promise]; + + return Promise.all(promises).then( + function onResolve([val1, val2, val3, val4]) { + do_check_eq(val1, 1); + do_check_eq(val2, 2); + do_check_eq(val3, 3); + do_check_eq(val4, 4); + } + ); + })); + +// Test that rejecting one of the promises passed to Promise.all() +// rejects the promise return by Promise.all() +tests.push( + make_promise_test(function all_reject(test) { + let error = new Error("Boom"); + + let d1 = Promise.defer(); + let d2 = Promise.defer(); + let d3 = Promise.defer(); + + d3.resolve(3); + d2.resolve(2); + do_execute_soon(() => d1.reject(error)); + + let promises = [d1.promise, d2.promise, d3.promise]; + + return Promise.all(promises).then( + function onResolve() { + do_throw("Incorrect call to onResolve listener"); + }, + function onReject(reason) { + do_check_eq(reason, error, "Rejection lead to the expected reason"); + } + ); + })); + +// Test that passing only values (not promises) to Promise.all() +// forwards them all as resolution values. +tests.push( + make_promise_test(function all_resolve_no_promises(test) { + try { + Promise.all(null); + do_check_true(false, "all() should only accept iterables"); + } catch (e) { + do_check_true(true, "all() fails when first the arg is not an iterable"); + } + + let p1 = Promise.all([]).then( + function onResolve(val) { + do_check_true(Array.isArray(val) && val.length == 0); + } + ); + + let p2 = Promise.all([1, 2, 3]).then( + function onResolve([val1, val2, val3]) { + do_check_eq(val1, 1); + do_check_eq(val2, 2); + do_check_eq(val3, 3); + } + ); + + return Promise.all([p1, p2]); + })); + +// Test that Promise.all() handles non-array iterables +tests.push( + make_promise_test(function all_iterable(test) { + function* iterable() { + yield 1; + yield 2; + yield 3; + } + + return Promise.all(iterable()).then( + function onResolve([val1, val2, val3]) { + do_check_eq(val1, 1); + do_check_eq(val2, 2); + do_check_eq(val3, 3); + }, + function onReject() { + do_throw("all() unexpectedly rejected"); + } + ); + })); + +// Test that throwing from the iterable passed to Promise.all() rejects the +// promise returned by Promise.all() +tests.push( + make_promise_test(function all_iterable_throws(test) { + function* iterable() { + throw 1; + } + + return Promise.all(iterable()).then( + function onResolve() { + do_throw("all() unexpectedly resolved"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "all() rejects when the iterator throws"); + } + ); + })); + +// Test that Promise.race() resolves with the first available resolution value +tests.push( + make_promise_test(function race_resolve(test) { + let p1 = Promise.resolve(1); + let p2 = Promise.resolve().then(() => 2); + + return Promise.race([p1, p2]).then( + function onResolve(value) { + do_check_eq(value, 1); + } + ); + })); + +// Test that passing only values (not promises) to Promise.race() works +tests.push( + make_promise_test(function race_resolve_no_promises(test) { + try { + Promise.race(null); + do_check_true(false, "race() should only accept iterables"); + } catch (e) { + do_check_true(true, "race() fails when first the arg is not an iterable"); + } + + return Promise.race([1, 2, 3]).then( + function onResolve(value) { + do_check_eq(value, 1); + } + ); + })); + +// Test that Promise.race() never resolves when passed an empty iterable +tests.push( + make_promise_test(function race_resolve_never(test) { + return new Promise(resolve => { + Promise.race([]).then( + function onResolve() { + do_throw("race() unexpectedly resolved"); + }, + function onReject() { + do_throw("race() unexpectedly rejected"); + } + ); + + // Approximate "never" so we don't have to solve the halting problem. + do_timeout(200, resolve); + }); + })); + +// Test that Promise.race() handles non-array iterables. +tests.push( + make_promise_test(function race_iterable(test) { + function* iterable() { + yield 1; + yield 2; + yield 3; + } + + return Promise.race(iterable()).then( + function onResolve(value) { + do_check_eq(value, 1); + }, + function onReject() { + do_throw("race() unexpectedly rejected"); + } + ); + })); + +// Test that throwing from the iterable passed to Promise.race() rejects the +// promise returned by Promise.race() +tests.push( + make_promise_test(function race_iterable_throws(test) { + function* iterable() { + throw 1; + } + + return Promise.race(iterable()).then( + function onResolve() { + do_throw("race() unexpectedly resolved"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "race() rejects when the iterator throws"); + } + ); + })); + +// Test that rejecting one of the promises passed to Promise.race() rejects the +// promise returned by Promise.race() +tests.push( + make_promise_test(function race_reject(test) { + let p1 = Promise.reject(1); + let p2 = Promise.resolve(2); + let p3 = Promise.resolve(3); + + return Promise.race([p1, p2, p3]).then( + function onResolve() { + do_throw("race() unexpectedly resolved"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "race() rejects when given a rejected promise"); + } + ); + })); + +// Test behavior of the Promise constructor. +tests.push( + make_promise_test(function test_constructor(test) { + try { + new Promise(null); + do_check_true(false, "Constructor should fail when not passed a function"); + } catch (e) { + do_check_true(true, "Constructor fails when not passed a function"); + } + + let executorRan = false; + let promise = new Promise( + function executor(resolve, reject) { + executorRan = true; + do_check_eq(this, undefined); + do_check_eq(typeof resolve, "function", + "resolve function should be passed to the executor"); + do_check_eq(typeof reject, "function", + "reject function should be passed to the executor"); + } + ); + do_check_instanceof(promise, Promise); + do_check_true(executorRan, "Executor should execute synchronously"); + + // resolve a promise from the executor + let resolvePromise = new Promise( + function executor(resolve) { + resolve(1); + } + ).then( + function onResolve(value) { + do_check_eq(value, 1, "Executor resolved with correct value"); + }, + function onReject() { + do_throw("Executor unexpectedly rejected"); + } + ); + + // reject a promise from the executor + let rejectPromise = new Promise( + function executor(_, reject) { + reject(1); + } + ).then( + function onResolve() { + do_throw("Executor unexpectedly resolved"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "Executor rejected with correct value"); + } + ); + + // throw from the executor, causing a rejection + let throwPromise = new Promise( + function executor() { + throw 1; + } + ).then( + function onResolve() { + do_throw("Throwing inside an executor should not resolve the promise"); + }, + function onReject(reason) { + do_check_eq(reason, 1, "Executor rejected with correct value"); + } + ); + + return Promise.all([resolvePromise, rejectPromise, throwPromise]); + })); + +// Test deadlock in Promise.jsm with nested event loops +// The scenario being tested is: +// promise_1.then({ +// do some work that will asynchronously signal done +// start an event loop waiting for the done signal +// } +// where the async work uses resolution of a second promise to +// trigger the "done" signal. While this would likely work in a +// naive implementation, our constant-stack implementation needs +// a special case to avoid deadlock. Note that this test is +// sensitive to the implementation-dependent order in which then() +// clauses for two different promises are executed, so it is +// possible for other implementations to pass this test and still +// have similar deadlocks. +tests.push( + make_promise_test(function promise_nested_eventloop_deadlock(test) { + // Set up a (long enough to be noticeable) timeout to + // exit the nested event loop and throw if the test run is hung + let shouldExitNestedEventLoop = false; + + function event_loop() { + let thr = Services.tm.mainThread; + while (!shouldExitNestedEventLoop) { + thr.processNextEvent(true); + } + } + + // I wish there was a way to cancel xpcshell do_timeout()s + do_timeout(2000, () => { + if (!shouldExitNestedEventLoop) { + shouldExitNestedEventLoop = true; + do_throw("Test timed out"); + } + }); + + let promise1 = Promise.resolve(1); + let promise2 = Promise.resolve(2); + + do_print("Setting wait for first promise"); + promise1.then(value => { + do_print("Starting event loop"); + event_loop(); + }, null); + + do_print("Setting wait for second promise"); + return promise2.catch(error => { return 3; }) + .then( + count => { + shouldExitNestedEventLoop = true; + }); + })); + +function wait_for_uncaught(aMustAppear, aTimeout = undefined) { + let remaining = new Set(); + for (let k of aMustAppear) { + remaining.add(k); + } + let deferred = Promise.defer(); + let print = do_print; + let execute_soon = do_execute_soon; + let observer = function({message, stack}) { + let data = message + stack; + print("Observing " + message + ", looking for " + aMustAppear.join(", ")); + for (let expected of remaining) { + if (data.indexOf(expected) != -1) { + print("I found " + expected); + remaining.delete(expected); + } + if (remaining.size == 0 && observer) { + Promise.Debugging.removeUncaughtErrorObserver(observer); + observer = null; + deferred.resolve(); + } + } + }; + Promise.Debugging.addUncaughtErrorObserver(observer); + if (aTimeout) { + do_timeout(aTimeout, function timeout() { + if (observer) { + Promise.Debugging.removeUncaughtErrorObserver(observer); + observer = null; + } + deferred.reject(new Error("Timeout")); + }); + } + return deferred.promise; +} + +// Test that uncaught errors are reported as uncaught +(function() { + let make_string_rejection = function make_string_rejection() { + let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); + let string = "This is an uncaught rejection " + salt; + // Our error is not Error-like nor an nsIException, so the stack will + // include the closure doing the actual rejection. + return {mustFind: ["test_rejection_closure", string], error: string}; + }; + let make_num_rejection = function make_num_rejection() { + let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); + // Our error is not Error-like nor an nsIException, so the stack will + // include the closure doing the actual rejection. + return {mustFind: ["test_rejection_closure", salt], error: salt}; + }; + let make_undefined_rejection = function make_undefined_rejection() { + // Our error is not Error-like nor an nsIException, so the stack will + // include the closure doing the actual rejection. + return {mustFind: ["test_rejection_closure"], error: undefined}; + }; + let make_error_rejection = function make_error_rejection() { + let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); + let error = new Error("This is an uncaught error " + salt); + return { + mustFind: [error.message, error.fileName, error.lineNumber, error.stack], + error: error + }; + }; + let make_exception_rejection = function make_exception_rejection() { + let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); + let exn = new Components.Exception("This is an uncaught exception " + salt, + Components.results.NS_ERROR_NOT_AVAILABLE); + return { + mustFind: [exn.message, exn.filename, exn.lineNumber, exn.location.toString()], + error: exn + }; + }; + for (let make_rejection of [make_string_rejection, + make_num_rejection, + make_undefined_rejection, + make_error_rejection, + make_exception_rejection]) { + let {mustFind, error} = make_rejection(); + let name = make_rejection.name; + tests.push(make_promise_test(function test_uncaught_is_reported() { + do_print("Testing with rejection " + name); + let promise = wait_for_uncaught(mustFind); + (function test_rejection_closure() { + // For the moment, we cannot be absolutely certain that a value is + // garbage-collected, even if it is not referenced anymore, due to + // the conservative stack-scanning algorithm. + // + // To be _almost_ certain that a value will be garbage-collected, we + // 1. isolate that value in an anonymous closure; + // 2. allocate 100 values instead of 1 (gc-ing a single value from + // these is sufficient for the test); + // 3. place everything in a loop, as the JIT typically reuses memory; + // 4. call all the GC methods we can. + // + // Unfortunately, we might still have intermittent failures, + // materialized as timeouts. + // + for (let i = 0; i < 100; ++i) { + Promise.reject(error); + } + })(); + do_print("Posted all rejections"); + Components.utils.forceGC(); + Components.utils.forceCC(); + Components.utils.forceShrinkingGC(); + return promise; + })); + } +})(); + + +// Test that caught errors are not reported as uncaught +tests.push( +make_promise_test(function test_caught_is_not_reported() { + let salt = (Math.random() * ( Math.pow(2, 24) - 1 )); + let promise = wait_for_uncaught([salt], 500); + (function() { + let uncaught = Promise.reject("This error, on the other hand, is caught " + salt); + uncaught.catch(function() { /* ignore rejection */ }); + uncaught = null; + })(); + // Isolate this in a function to increase likelihood that the gc will + // realise that |uncaught| has remained uncaught. + Components.utils.forceGC(); + + return promise.then(function onSuccess() { + throw new Error("This error was caught and should not have been reported"); + }, function onError() { + do_print("The caught error was not reported, all is fine"); + } + ); +})); + +// Bug 1033406 - Make sure Promise works even after freezing. +tests.push( + make_promise_test(function test_freezing_promise(test) { + var p = new Promise(function executor(resolve) { + do_execute_soon(resolve); + }); + Object.freeze(p); + return p; + }) +); + +function run_test() +{ + do_test_pending(); + run_promise_tests(tests, do_test_finished); +} diff --git a/toolkit/modules/tests/xpcshell/test_PromiseUtils.js b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js new file mode 100644 index 000000000..c3ab839e4 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_PromiseUtils.js @@ -0,0 +1,105 @@ + /* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Components.utils.import("resource://gre/modules/PromiseUtils.jsm"); +Components.utils.import("resource://gre/modules/Timer.jsm"); +Components.utils.import("resource://testing-common/PromiseTestUtils.jsm"); + +// Tests for PromiseUtils.jsm +function run_test() { + run_next_test(); +} + +// Tests for PromiseUtils.defer() + +/* Tests for checking the resolve method of the Deferred object + * returned by PromiseUtils.defer() */ +add_task(function* test_resolve_string() { + let def = PromiseUtils.defer(); + let expected = "The promise is resolved " + Math.random(); + def.resolve(expected); + let result = yield def.promise; + Assert.equal(result, expected, "def.resolve() resolves the promise"); +}); + +/* Test for the case when undefined is passed to the resolve method + * of the Deferred object */ +add_task(function* test_resolve_undefined() { + let def = PromiseUtils.defer(); + def.resolve(); + let result = yield def.promise; + Assert.equal(result, undefined, "resolve works with undefined as well"); +}); + +/* Test when a pending promise is passed to the resolve method + * of the Deferred object */ +add_task(function* test_resolve_pending_promise() { + let def = PromiseUtils.defer(); + let expected = 100 + Math.random(); + let p = new Promise((resolve, reject) => { + setTimeout(() => resolve(expected), 100); + }); + def.resolve(p); + let result = yield def.promise; + Assert.equal(result, expected, "def.promise assumed the state of the passed promise"); +}); + +/* Test when a resovled promise is passed + * to the resolve method of the Deferred object */ +add_task(function* test_resolve_resolved_promise() { + let def = PromiseUtils.defer(); + let expected = "Yeah resolved" + Math.random(); + let p = new Promise((resolve, reject) => resolve(expected)); + def.resolve(p); + let result = yield def.promise; + Assert.equal(result, expected, "Resolved promise is passed to the resolve method"); +}); + +/* Test for the case when a rejected promise is + * passed to the resolve method */ +add_task(function* test_resolve_rejected_promise() { + let def = PromiseUtils.defer(); + let p = new Promise((resolve, reject) => reject(new Error("There its an rejection"))); + def.resolve(p); + yield Assert.rejects(def.promise, /There its an rejection/, "Settled rejection promise passed to the resolve method"); +}); + +/* Test for the checking the reject method of + * the Deferred object returned by PromiseUtils.defer() */ +add_task(function* test_reject_Error() { + let def = PromiseUtils.defer(); + def.reject(new Error("This one rejects")); + yield Assert.rejects(def.promise, /This one rejects/, "reject method with Error for rejection"); +}); + +/* Test for the case when a pending Promise is passed to + * the reject method of Deferred object */ +add_task(function* test_reject_pending_promise() { + let def = PromiseUtils.defer(); + let p = new Promise((resolve, reject) => { + setTimeout(() => resolve(100), 100); + }); + def.reject(p); + yield Assert.rejects(def.promise, Promise, "Rejection with a pending promise uses the passed promise itself as the reason of rejection"); +}); + +/* Test for the case when a resolved Promise + * is passed to the reject method */ +add_task(function* test_reject_resolved_promise() { + let def = PromiseUtils.defer(); + let p = new Promise((resolve, reject) => resolve("This resolved")); + def.reject(p); + yield Assert.rejects(def.promise, Promise, "Rejection with a resolved promise uses the passed promise itself as the reason of rejection"); +}); + +/* Test for the case when a rejected Promise is + * passed to the reject method */ +add_task(function* test_reject_resolved_promise() { + PromiseTestUtils.expectUncaughtRejection(/This one rejects/); + let def = PromiseUtils.defer(); + let p = new Promise((resolve, reject) => reject(new Error("This one rejects"))); + def.reject(p); + yield Assert.rejects(def.promise, Promise, "Rejection with a rejected promise uses the passed promise itself as the reason of rejection"); +}); diff --git a/toolkit/modules/tests/xpcshell/test_Services.js b/toolkit/modules/tests/xpcshell/test_Services.js new file mode 100644 index 000000000..a50ecca3d --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_Services.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the Services.jsm module. + */ + +// Globals + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function checkService(service, interface) { + do_print("Checking that Services." + service + " is an " + interface); + do_check_true(service in Services); + do_check_true(Services[service] instanceof interface); +} + +// Tests + +function run_test() +{ + do_get_profile(); + + checkService("appShell", Ci.nsIAppShellService); + checkService("appinfo", Ci.nsIXULRuntime); + checkService("blocklist", Ci.nsIBlocklistService); + checkService("cache", Ci.nsICacheService); + checkService("cache2", Ci.nsICacheStorageService); + checkService("clipboard", Ci.nsIClipboard); + checkService("console", Ci.nsIConsoleService); + checkService("contentPrefs", Ci.nsIContentPrefService); + checkService("cookies", Ci.nsICookieManager2); + checkService("dirsvc", Ci.nsIDirectoryService); + checkService("dirsvc", Ci.nsIProperties); + checkService("DOMRequest", Ci.nsIDOMRequestService); + checkService("domStorageManager", Ci.nsIDOMStorageManager); + checkService("downloads", Ci.nsIDownloadManager); + checkService("droppedLinkHandler", Ci.nsIDroppedLinkHandler); + checkService("eTLD", Ci.nsIEffectiveTLDService); + checkService("focus", Ci.nsIFocusManager); + checkService("io", Ci.nsIIOService); + checkService("io", Ci.nsIIOService2); + checkService("locale", Ci.nsILocaleService); + checkService("logins", Ci.nsILoginManager); + checkService("obs", Ci.nsIObserverService); + checkService("perms", Ci.nsIPermissionManager); + checkService("prefs", Ci.nsIPrefBranch); + checkService("prefs", Ci.nsIPrefService); + checkService("prompt", Ci.nsIPromptService); + checkService("scriptSecurityManager", Ci.nsIScriptSecurityManager); + checkService("scriptloader", Ci.mozIJSSubScriptLoader); + checkService("startup", Ci.nsIAppStartup); + checkService("storage", Ci.mozIStorageService); + checkService("strings", Ci.nsIStringBundleService); + checkService("sysinfo", Ci.nsIPropertyBag2); + checkService("telemetry", Ci.nsITelemetry); + checkService("tm", Ci.nsIThreadManager); + checkService("uriFixup", Ci.nsIURIFixup); + checkService("urlFormatter", Ci.nsIURLFormatter); + checkService("vc", Ci.nsIVersionComparator); + checkService("wm", Ci.nsIWindowMediator); + checkService("ww", Ci.nsIWindowWatcher); + if ("nsIBrowserSearchService" in Ci) { + checkService("search", Ci.nsIBrowserSearchService); + } + if ("nsIAndroidBridge" in Ci) { + checkService("androidBridge", Ci.nsIAndroidBridge); + } + + // In xpcshell tests, the "@mozilla.org/xre/app-info;1" component implements + // only the nsIXULRuntime interface, but not nsIXULAppInfo. To test the + // service getter for the latter interface, load mock app-info. + let tmp = {}; + Cu.import("resource://testing-common/AppInfo.jsm", tmp); + tmp.updateAppInfo(); + + // We need to reload the module to update the lazy getter. + Cu.unload("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/Services.jsm"); + + checkService("appinfo", Ci.nsIXULAppInfo); + + Cu.unload("resource://gre/modules/Services.jsm"); +} diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js new file mode 100644 index 000000000..75d7a1992 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_updatechannel.js @@ -0,0 +1,38 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { utils: Cu } = Components; + +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/UpdateUtils.jsm"); + +const PREF_APP_UPDATE_CHANNEL = "app.update.channel"; +const TEST_CHANNEL = "TestChannel"; +const PREF_PARTNER_A = "app.partner.test_partner_a"; +const TEST_PARTNER_A = "TestPartnerA"; +const PREF_PARTNER_B = "app.partner.test_partner_b"; +const TEST_PARTNER_B = "TestPartnerB"; + +add_task(function* test_updatechannel() { + let defaultPrefs = new Preferences({ defaultBranch: true }); + let currentChannel = defaultPrefs.get(PREF_APP_UPDATE_CHANNEL); + + do_check_eq(UpdateUtils.UpdateChannel, currentChannel); + do_check_eq(UpdateUtils.getUpdateChannel(true), currentChannel); + do_check_eq(UpdateUtils.getUpdateChannel(false), currentChannel); + + defaultPrefs.set(PREF_APP_UPDATE_CHANNEL, TEST_CHANNEL); + do_check_eq(UpdateUtils.UpdateChannel, TEST_CHANNEL); + do_check_eq(UpdateUtils.getUpdateChannel(true), TEST_CHANNEL); + do_check_eq(UpdateUtils.getUpdateChannel(false), TEST_CHANNEL); + + defaultPrefs.set(PREF_PARTNER_A, TEST_PARTNER_A); + defaultPrefs.set(PREF_PARTNER_B, TEST_PARTNER_B); + do_check_eq(UpdateUtils.UpdateChannel, + TEST_CHANNEL + "-cck-" + TEST_PARTNER_A + "-" + TEST_PARTNER_B); + do_check_eq(UpdateUtils.getUpdateChannel(true), + TEST_CHANNEL + "-cck-" + TEST_PARTNER_A + "-" + TEST_PARTNER_B); + do_check_eq(UpdateUtils.getUpdateChannel(false), TEST_CHANNEL); +}); diff --git a/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js new file mode 100644 index 000000000..da5d868e3 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_UpdateUtils_url.js @@ -0,0 +1,292 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/UpdateUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://testing-common/AppInfo.jsm"); +Cu.import("resource://gre/modules/ctypes.jsm"); + +const PREF_APP_UPDATE_CHANNEL = "app.update.channel"; +const PREF_APP_PARTNER_BRANCH = "app.partner."; +const PREF_DISTRIBUTION_ID = "distribution.id"; +const PREF_DISTRIBUTION_VERSION = "distribution.version"; + +const URL_PREFIX = "http://localhost/"; + +const MSG_SHOULD_EQUAL = " should equal the expected value"; + +updateAppInfo(); +const gAppInfo = getAppInfo(); +const gDefaultPrefBranch = Services.prefs.getDefaultBranch(null); + +function setUpdateChannel(aChannel) { + gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, aChannel); +} + +function getServicePack() { + // NOTE: This function is a helper function and not a test. Thus, + // it uses throw() instead of do_throw(). Any tests that use this function + // should catch exceptions thrown in this function and deal with them + // appropriately (usually by calling do_throw). + const BYTE = ctypes.uint8_t; + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const WCHAR = ctypes.char16_t; + const BOOL = ctypes.int; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724833%28v=vs.85%29.aspx + const SZCSDVERSIONLENGTH = 128; + const OSVERSIONINFOEXW = new ctypes.StructType('OSVERSIONINFOEXW', + [ + {dwOSVersionInfoSize: DWORD}, + {dwMajorVersion: DWORD}, + {dwMinorVersion: DWORD}, + {dwBuildNumber: DWORD}, + {dwPlatformId: DWORD}, + {szCSDVersion: ctypes.ArrayType(WCHAR, SZCSDVERSIONLENGTH)}, + {wServicePackMajor: WORD}, + {wServicePackMinor: WORD}, + {wSuiteMask: WORD}, + {wProductType: BYTE}, + {wReserved: BYTE} + ]); + + let kernel32 = ctypes.open("kernel32"); + try { + let GetVersionEx = kernel32.declare("GetVersionExW", + ctypes.default_abi, + BOOL, + OSVERSIONINFOEXW.ptr); + let winVer = OSVERSIONINFOEXW(); + winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size; + + if (0 === GetVersionEx(winVer.address())) { + // Using "throw" instead of "do_throw" (see NOTE above) + throw ("Failure in GetVersionEx (returned 0)"); + } + + return winVer.wServicePackMajor + "." + winVer.wServicePackMinor; + } finally { + kernel32.close(); + } +} + +function getProcArchitecture() { + // NOTE: This function is a helper function and not a test. Thus, + // it uses throw() instead of do_throw(). Any tests that use this function + // should catch exceptions thrown in this function and deal with them + // appropriately (usually by calling do_throw). + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + + // This structure is described at: + // http://msdn.microsoft.com/en-us/library/ms724958%28v=vs.85%29.aspx + const SYSTEM_INFO = new ctypes.StructType('SYSTEM_INFO', + [ + {wProcessorArchitecture: WORD}, + {wReserved: WORD}, + {dwPageSize: DWORD}, + {lpMinimumApplicationAddress: ctypes.voidptr_t}, + {lpMaximumApplicationAddress: ctypes.voidptr_t}, + {dwActiveProcessorMask: DWORD.ptr}, + {dwNumberOfProcessors: DWORD}, + {dwProcessorType: DWORD}, + {dwAllocationGranularity: DWORD}, + {wProcessorLevel: WORD}, + {wProcessorRevision: WORD} + ]); + + let kernel32 = ctypes.open("kernel32"); + try { + let GetNativeSystemInfo = kernel32.declare("GetNativeSystemInfo", + ctypes.default_abi, + ctypes.void_t, + SYSTEM_INFO.ptr); + let sysInfo = SYSTEM_INFO(); + // Default to unknown + sysInfo.wProcessorArchitecture = 0xffff; + + GetNativeSystemInfo(sysInfo.address()); + switch (sysInfo.wProcessorArchitecture) { + case 9: + return "x64"; + case 6: + return "IA64"; + case 0: + return "x86"; + default: + // Using "throw" instead of "do_throw" (see NOTE above) + throw ("Unknown architecture returned from GetNativeSystemInfo: " + sysInfo.wProcessorArchitecture); + } + } finally { + kernel32.close(); + } +} + +// Helper function for formatting a url and getting the result we're +// interested in +function getResult(url) { + url = UpdateUtils.formatUpdateURL(url); + return url.substr(URL_PREFIX.length).split("/")[0]; +} + +// url constructed with %PRODUCT% +add_task(function* test_product() { + let url = URL_PREFIX + "%PRODUCT%/"; + Assert.equal(getResult(url), gAppInfo.name, + "the url param for %PRODUCT%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %VERSION% +add_task(function* test_version() { + let url = URL_PREFIX + "%VERSION%/"; + Assert.equal(getResult(url), gAppInfo.version, + "the url param for %VERSION%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %BUILD_ID% +add_task(function* test_build_id() { + let url = URL_PREFIX + "%BUILD_ID%/"; + Assert.equal(getResult(url), gAppInfo.appBuildID, + "the url param for %BUILD_ID%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %BUILD_TARGET% +// XXX TODO - it might be nice if we tested the actual ABI +add_task(function* test_build_target() { + let url = URL_PREFIX + "%BUILD_TARGET%/"; + + let abi; + try { + abi = gAppInfo.XPCOMABI; + } catch (e) { + do_throw("nsIXULAppInfo:XPCOMABI not defined\n"); + } + + if (AppConstants.platform == "macosx") { + // Mac universal build should report a different ABI than either macppc + // or mactel. This is necessary since nsUpdateService.js will set the ABI to + // Universal-gcc3 for Mac universal builds. + let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"]. + getService(Ci.nsIMacUtils); + + if (macutils.isUniversalBinary) { + abi += "-u-" + macutils.architecturesInBinary; + } + } else if (AppConstants.platform == "win") { + // Windows build should report the CPU architecture that it's running on. + abi += "-" + getProcArchitecture(); + } + + Assert.equal(getResult(url), gAppInfo.OS + "_" + abi, + "the url param for %BUILD_TARGET%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %LOCALE% +// Bug 488936 added the update.locale file that stores the update locale +add_task(function* test_locale() { + // The code that gets the locale accesses the profile which is only available + // after calling do_get_profile in xpcshell tests. This prevents an error from + // being logged. + do_get_profile(); + + let url = URL_PREFIX + "%LOCALE%/"; + Assert.equal(getResult(url), AppConstants.INSTALL_LOCALE, + "the url param for %LOCALE%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %CHANNEL% +add_task(function* test_channel() { + let url = URL_PREFIX + "%CHANNEL%/"; + setUpdateChannel("test_channel"); + Assert.equal(getResult(url), "test_channel", + "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %CHANNEL% with distribution partners +add_task(function* test_channel_distribution() { + let url = URL_PREFIX + "%CHANNEL%/"; + gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner1", + "test_partner1"); + gDefaultPrefBranch.setCharPref(PREF_APP_PARTNER_BRANCH + "test_partner2", + "test_partner2"); + Assert.equal(getResult(url), + "test_channel-cck-test_partner1-test_partner2", + "the url param for %CHANNEL%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %PLATFORM_VERSION% +add_task(function* test_platform_version() { + let url = URL_PREFIX + "%PLATFORM_VERSION%/"; + Assert.equal(getResult(url), gAppInfo.platformVersion, + "the url param for %PLATFORM_VERSION%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %OS_VERSION% +add_task(function* test_os_version() { + let url = URL_PREFIX + "%OS_VERSION%/"; + let osVersion; + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); + osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version"); + + if (AppConstants.platform == "win") { + try { + let servicePack = getServicePack(); + osVersion += "." + servicePack; + } catch (e) { + do_throw("Failure obtaining service pack: " + e); + } + + if ("5.0" === sysInfo.getProperty("version")) { // Win2K + osVersion += " (unknown)"; + } else { + try { + osVersion += " (" + getProcArchitecture() + ")"; + } catch (e) { + do_throw("Failed to obtain processor architecture: " + e); + } + } + } + + if (osVersion) { + try { + osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")"; + } catch (e) { + // Not all platforms have a secondary widget library, so an error is + // nothing to worry about. + } + osVersion = encodeURIComponent(osVersion); + } + + Assert.equal(getResult(url), osVersion, + "the url param for %OS_VERSION%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %DISTRIBUTION% +add_task(function* test_distribution() { + let url = URL_PREFIX + "%DISTRIBUTION%/"; + gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_ID, "test_distro"); + Assert.equal(getResult(url), "test_distro", + "the url param for %DISTRIBUTION%" + MSG_SHOULD_EQUAL); +}); + +// url constructed with %DISTRIBUTION_VERSION% +add_task(function* test_distribution_version() { + let url = URL_PREFIX + "%DISTRIBUTION_VERSION%/"; + gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_VERSION, "test_distro_version"); + Assert.equal(getResult(url), "test_distro_version", + "the url param for %DISTRIBUTION_VERSION%" + MSG_SHOULD_EQUAL); +}); + +add_task(function* test_custom() { + Services.prefs.setCharPref("app.update.custom", "custom"); + let url = URL_PREFIX + "%CUSTOM%/"; + Assert.equal(getResult(url), "custom", + "the url query string for %CUSTOM%" + MSG_SHOULD_EQUAL); +}); diff --git a/toolkit/modules/tests/xpcshell/test_ZipUtils.js b/toolkit/modules/tests/xpcshell/test_ZipUtils.js new file mode 100644 index 000000000..71c6884d4 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_ZipUtils.js @@ -0,0 +1,79 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ARCHIVE = "zips/zen.zip"; +const SUBDIR = "zen"; +const SYMLINK = "beyond_link"; +const ENTRIES = ["beyond.txt", SYMLINK, "waterwood.txt"]; + +Components.utils.import("resource://gre/modules/ZipUtils.jsm"); +Components.utils.import("resource://gre/modules/FileUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const archive = do_get_file(ARCHIVE, false); +const dir = do_get_profile().clone(); +dir.append("test_ZipUtils"); + +function run_test() { + run_next_test(); +} + +function ensureExtracted(target) { + target.append(SUBDIR); + do_check_true(target.exists()); + + for (let i = 0; i < ENTRIES.length; i++) { + let entry = target.clone(); + entry.append(ENTRIES[i]); + do_print("ENTRY " + entry.path); + do_check_true(entry.exists()); + } +} + +function ensureHasSymlink(target) { + // Just bail out if running on Windows, since symlinks do not exists there. + if (Services.appinfo.OS === "WINNT") { + return; + } + + let entry = target.clone(); + entry.append(SYMLINK); + + do_print("ENTRY " + entry.path); + do_check_true(entry.exists()); + do_check_true(entry.isSymlink()); +} + +add_task(function test_extractFiles() { + let target = dir.clone(); + target.append("test_extractFiles"); + + try { + ZipUtils.extractFiles(archive, target); + } catch (e) { + do_throw("Failed to extract synchronously!"); + } + + ensureExtracted(target); + ensureHasSymlink(target); +}); + +add_task(function* test_extractFilesAsync() { + let target = dir.clone(); + target.append("test_extractFilesAsync"); + target.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + + yield ZipUtils.extractFilesAsync(archive, target).then( + function success() { + do_print("SUCCESS"); + ensureExtracted(target); + }, + function failure() { + do_print("FAILURE"); + do_throw("Failed to extract asynchronously!"); + } + ); +}); diff --git a/toolkit/modules/tests/xpcshell/test_client_id.js b/toolkit/modules/tests/xpcshell/test_client_id.js new file mode 100644 index 000000000..10ef2a3ea --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_client_id.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/ClientID.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); + +function run_test() { + do_get_profile(); + run_next_test(); +} + +add_task(function* () { + const drsPath = OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "state.json"); + const fhrDir = OS.Path.join(OS.Constants.Path.profileDir, "healthreport"); + const fhrPath = OS.Path.join(fhrDir, "state.json"); + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + const invalidIDs = [-1, 0.5, "INVALID-UUID", true, "", "3d1e1560-682a-4043-8cf2-aaaaaaaaaaaZ"]; + const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID"; + + yield OS.File.makeDir(fhrDir); + + // Check that we are importing the FHR client ID. + let clientID = CommonUtils.generateUUID(); + yield CommonUtils.writeJSON({clientID: clientID}, fhrPath); + Assert.equal(clientID, yield ClientID.getClientID()); + + // We should persist the ID in DRS now and not pick up a differing ID from FHR. + yield ClientID._reset(); + yield CommonUtils.writeJSON({clientID: CommonUtils.generateUUID()}, fhrPath); + Assert.equal(clientID, yield ClientID.getClientID()); + + // We should be guarded against broken FHR data. + for (let invalidID of invalidIDs) { + yield ClientID._reset(); + yield OS.File.remove(drsPath); + yield CommonUtils.writeJSON({clientID: invalidID}, fhrPath); + clientID = yield ClientID.getClientID(); + Assert.equal(typeof(clientID), 'string'); + Assert.ok(uuidRegex.test(clientID)); + } + + // We should be guarded against invalid FHR json. + yield ClientID._reset(); + yield OS.File.remove(drsPath); + yield OS.File.writeAtomic(fhrPath, "abcd", {encoding: "utf-8", tmpPath: fhrPath + ".tmp"}); + clientID = yield ClientID.getClientID(); + Assert.equal(typeof(clientID), 'string'); + Assert.ok(uuidRegex.test(clientID)); + + // We should be guarded against broken DRS data too and fall back to loading + // the FHR ID. + for (let invalidID of invalidIDs) { + yield ClientID._reset(); + clientID = CommonUtils.generateUUID(); + yield CommonUtils.writeJSON({clientID: clientID}, fhrPath); + yield CommonUtils.writeJSON({clientID: invalidID}, drsPath); + Assert.equal(clientID, yield ClientID.getClientID()); + } + + // We should be guarded against invalid DRS json too. + yield ClientID._reset(); + yield OS.File.remove(fhrPath); + yield OS.File.writeAtomic(drsPath, "abcd", {encoding: "utf-8", tmpPath: drsPath + ".tmp"}); + clientID = yield ClientID.getClientID(); + Assert.equal(typeof(clientID), 'string'); + Assert.ok(uuidRegex.test(clientID)); + + // If both the FHR and DSR data are broken, we should end up with a new client ID. + for (let invalidID of invalidIDs) { + yield ClientID._reset(); + yield CommonUtils.writeJSON({clientID: invalidID}, fhrPath); + yield CommonUtils.writeJSON({clientID: invalidID}, drsPath); + clientID = yield ClientID.getClientID(); + Assert.equal(typeof(clientID), 'string'); + Assert.ok(uuidRegex.test(clientID)); + } + + // Assure that cached IDs are being checked for validity. + for (let invalidID of invalidIDs) { + yield ClientID._reset(); + Preferences.set(PREF_CACHED_CLIENTID, invalidID); + let cachedID = ClientID.getCachedClientID(); + Assert.strictEqual(cachedID, null, "ClientID should ignore invalid cached IDs"); + let prefID = Preferences.get(PREF_CACHED_CLIENTID, null); + Assert.strictEqual(prefID, null, "ClientID should reset invalid cached IDs"); + } +}); diff --git a/toolkit/modules/tests/xpcshell/test_jsesc.js b/toolkit/modules/tests/xpcshell/test_jsesc.js new file mode 100644 index 000000000..0c6cbba69 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_jsesc.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/third_party/jsesc/jsesc.js"); + +function run_test() { + do_check_eq(jsesc("teééést", {lowercaseHex: true}), "te\\xe9\\xe9\\xe9st"); + do_check_eq(jsesc("teééést", {lowercaseHex: false}), "te\\xE9\\xE9\\xE9st"); +} diff --git a/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js b/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js new file mode 100644 index 000000000..9ccf50b73 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_propertyListsUtils.js @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +Components.utils.import("resource://gre/modules/PropertyListUtils.jsm"); + +function checkValue(aPropertyListObject, aType, aValue) { + do_check_eq(PropertyListUtils.getObjectType(aPropertyListObject), aType); + if (aValue !== undefined) { + // Perform strict equality checks until Bug 714467 is fixed. + let strictEqualityCheck = function(a, b) { + do_check_eq(typeof(a), typeof(b)); + do_check_eq(a, b); + }; + + if (typeof(aPropertyListObject) == "object") + strictEqualityCheck(aPropertyListObject.valueOf(), aValue.valueOf()); + else + strictEqualityCheck(aPropertyListObject, aValue); + } +} + +function checkLazyGetterValue(aObject, aPropertyName, aType, aValue) { + let descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName); + do_check_eq(typeof(descriptor.get), "function"); + do_check_eq(typeof(descriptor.value), "undefined"); + checkValue(aObject[aPropertyName], aType, aValue); + descriptor = Object.getOwnPropertyDescriptor(aObject, aPropertyName); + do_check_eq(typeof(descriptor.get), "undefined"); + do_check_neq(typeof(descriptor.value), "undefined"); +} + +function checkMainPropertyList(aPropertyListRoot) { + const PRIMITIVE = PropertyListUtils.TYPE_PRIMITIVE; + + checkValue(aPropertyListRoot, PropertyListUtils.TYPE_DICTIONARY); + + // Check .has() + Assert.ok(aPropertyListRoot.has("Boolean")); + Assert.ok(!aPropertyListRoot.has("Nonexistent")); + + checkValue(aPropertyListRoot.get("Boolean"), PRIMITIVE, false); + + let array = aPropertyListRoot.get("Array"); + checkValue(array, PropertyListUtils.TYPE_ARRAY); + do_check_eq(array.length, 8); + + // Test both long and short values, since binary property lists store + // long values a little bit differently (see readDataLengthAndOffset). + + // Short ASCII string + checkLazyGetterValue(array, 0, PRIMITIVE, "abc"); + // Long ASCII string + checkLazyGetterValue(array, 1, PRIMITIVE, new Array(1001).join("a")); + // Short unicode string + checkLazyGetterValue(array, 2, PRIMITIVE, "\u05D0\u05D0\u05D0"); + // Long unicode string + checkLazyGetterValue(array, 3, PRIMITIVE, new Array(1001).join("\u05D0")); + // Unicode surrogate pair + checkLazyGetterValue(array, 4, PRIMITIVE, + "\uD800\uDC00\uD800\uDC00\uD800\uDC00"); + + // Date + checkLazyGetterValue(array, 5, PropertyListUtils.TYPE_DATE, + new Date("2011-12-31T11:15:23Z")); + + // Data + checkLazyGetterValue(array, 6, PropertyListUtils.TYPE_UINT8_ARRAY); + let dataAsString = Array.from(array[6]).map(b => String.fromCharCode(b)).join(""); + do_check_eq(dataAsString, "2011-12-31T11:15:33Z"); + + // Dict + let dict = array[7]; + checkValue(dict, PropertyListUtils.TYPE_DICTIONARY); + checkValue(dict.get("Negative Number"), PRIMITIVE, -400); + checkValue(dict.get("Real Number"), PRIMITIVE, 2.71828183); + checkValue(dict.get("Big Int"), + PropertyListUtils.TYPE_INT64, + "9007199254740993"); + checkValue(dict.get("Negative Big Int"), + PropertyListUtils.TYPE_INT64, + "-9007199254740993"); +} + +function readPropertyList(aFile, aCallback) { + PropertyListUtils.read(aFile, function(aPropertyListRoot) { + // Null root indicates failure to read property list. + // Note: It is important not to run do_check_n/eq directly on Dict and array + // objects, because it cases their toString to get invoked, doing away with + // all the lazy getter we'd like to test later. + do_check_true(aPropertyListRoot !== null); + aCallback(aPropertyListRoot); + run_next_test(); + }); +} + +function run_test() { + add_test(readPropertyList.bind(this, + do_get_file("propertyLists/bug710259_propertyListBinary.plist", false), + checkMainPropertyList)); + add_test(readPropertyList.bind(this, + do_get_file("propertyLists/bug710259_propertyListXML.plist", false), + checkMainPropertyList)); + run_next_test(); +} diff --git a/toolkit/modules/tests/xpcshell/test_readCertPrefs.js b/toolkit/modules/tests/xpcshell/test_readCertPrefs.js new file mode 100644 index 000000000..837a9912a --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_readCertPrefs.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/CertUtils.jsm"); + +const PREF_PREFIX = "certutils.certs."; + +function run_test() { + run_next_test(); +} + +function resetPrefs() { + var prefs = Services.prefs.getChildList(PREF_PREFIX); + prefs.forEach(Services.prefs.clearUserPref); +} + +function attributes_match(aCert, aExpected) { + if (Object.keys(aCert).length != Object.keys(aExpected).length) + return false; + + for (var attribute in aCert) { + if (!(attribute in aExpected)) + return false; + if (aCert[attribute] != aExpected[attribute]) + return false; + } + + return true; +} + +function test_results(aCerts, aExpected) { + do_check_eq(aCerts.length, aExpected.length); + + for (var i = 0; i < aCerts.length; i++) { + if (!attributes_match(aCerts[i], aExpected[i])) { + dump("Attributes for certificate " + (i + 1) + " did not match expected attributes\n"); + dump("Saw: " + aCerts[i].toSource() + "\n"); + dump("Expected: " + aExpected[i].toSource() + "\n"); + do_check_true(false); + } + } +} + +add_test(function test_singleCert() { + Services.prefs.setCharPref(PREF_PREFIX + "1.attribute1", "foo"); + Services.prefs.setCharPref(PREF_PREFIX + "1.attribute2", "bar"); + + var certs = readCertPrefs(PREF_PREFIX); + test_results(certs, [{ + attribute1: "foo", + attribute2: "bar" + }]); + + resetPrefs(); + run_next_test(); +}); + +add_test(function test_multipleCert() { + Services.prefs.setCharPref(PREF_PREFIX + "1.md5Fingerprint", "cf84a9a2a804e021f27cb5128fe151f4"); + Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert"); + Services.prefs.setCharPref(PREF_PREFIX + "2.md5Fingerprint", "9441051b7eb50e5ca2226095af710c1a"); + Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert"); + + var certs = readCertPrefs(PREF_PREFIX); + test_results(certs, [{ + md5Fingerprint: "cf84a9a2a804e021f27cb5128fe151f4", + nickname: "1st cert" + }, { + md5Fingerprint: "9441051b7eb50e5ca2226095af710c1a", + nickname: "2nd cert" + }]); + + resetPrefs(); + run_next_test(); +}); + +add_test(function test_skippedCert() { + Services.prefs.setCharPref(PREF_PREFIX + "1.issuerName", "Mozilla"); + Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert"); + Services.prefs.setCharPref(PREF_PREFIX + "2.issuerName", "Top CA"); + Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert"); + Services.prefs.setCharPref(PREF_PREFIX + "4.issuerName", "Unknown CA"); + Services.prefs.setCharPref(PREF_PREFIX + "4.nickname", "Ignored cert"); + + var certs = readCertPrefs(PREF_PREFIX); + test_results(certs, [{ + issuerName: "Mozilla", + nickname: "1st cert" + }, { + issuerName: "Top CA", + nickname: "2nd cert" + }]); + + resetPrefs(); + run_next_test(); +}); diff --git a/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js b/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js new file mode 100644 index 000000000..b3c8a443e --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_servicerequest_xhr.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/ServiceRequest.jsm"); + +add_task(function* test_tls_conservative() { + const request = new ServiceRequest(); + request.open("GET", "http://example.com", false); + + const sr_channel = request.channel.QueryInterface(Ci.nsIHttpChannelInternal); + ok(("beConservative" in sr_channel), "TLS setting is present in SR channel"); + ok(sr_channel.beConservative, "TLS setting in request channel is set to conservative for SR"); + + const xhr = new XMLHttpRequest(); + xhr.open("GET", "http://example.com", false); + + const xhr_channel = xhr.channel.QueryInterface(Ci.nsIHttpChannelInternal); + ok(("beConservative" in xhr_channel), "TLS setting is present in XHR channel"); + ok(!xhr_channel.beConservative, "TLS setting in request channel is not set to conservative for XHR"); + +}); diff --git a/toolkit/modules/tests/xpcshell/test_session_recorder.js b/toolkit/modules/tests/xpcshell/test_session_recorder.js new file mode 100644 index 000000000..dd9159c6e --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_session_recorder.js @@ -0,0 +1,306 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {utils: Cu} = Components; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/SessionRecorder.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-common/utils.js"); + + +function run_test() { + run_next_test(); +} + +function monkeypatchStartupInfo(recorder, start=new Date(), offset=500) { + Object.defineProperty(recorder, "_getStartupInfo", { + value: function _getStartupInfo() { + return { + process: start, + main: new Date(start.getTime() + offset), + firstPaint: new Date(start.getTime() + 2 * offset), + sessionRestored: new Date(start.getTime() + 3 * offset), + }; + } + }); +} + +function sleep(wait) { + let deferred = Promise.defer(); + + let timer = CommonUtils.namedTimer(function onTimer() { + deferred.resolve(); + }, wait, deferred.promise, "_sleepTimer"); + + return deferred.promise; +} + +function getRecorder(name, start, offset) { + let recorder = new SessionRecorder("testing." + name + "."); + monkeypatchStartupInfo(recorder, start, offset); + + return recorder; +} + +add_test(function test_basic() { + let recorder = getRecorder("basic"); + recorder.onStartup(); + recorder.onShutdown(); + + run_next_test(); +}); + +add_task(function* test_current_properties() { + let now = new Date(); + let recorder = getRecorder("current_properties", now); + yield sleep(25); + recorder.onStartup(); + + do_check_eq(recorder.startDate.getTime(), now.getTime()); + do_check_eq(recorder.activeTicks, 0); + do_check_true(recorder.fineTotalTime > 0); + do_check_eq(recorder.main, 500); + do_check_eq(recorder.firstPaint, 1000); + do_check_eq(recorder.sessionRestored, 1500); + + recorder.incrementActiveTicks(); + do_check_eq(recorder.activeTicks, 1); + + recorder._startDate = new Date(Date.now() - 1000); + recorder.updateTotalTime(); + do_check_eq(recorder.totalTime, 1); + + recorder.onShutdown(); +}); + +// If startup info isn't present yet, we should install a timer and get +// it eventually. +add_task(function* test_current_availability() { + let recorder = new SessionRecorder("testing.current_availability."); + let now = new Date(); + + Object.defineProperty(recorder, "_getStartupInfo", { + value: function _getStartupInfo() { + return { + process: now, + main: new Date(now.getTime() + 500), + firstPaint: new Date(now.getTime() + 1000), + }; + }, + writable: true, + }); + + Object.defineProperty(recorder, "STARTUP_RETRY_INTERVAL_MS", { + value: 100, + }); + + let oldRecord = recorder.recordStartupFields; + let recordCount = 0; + + Object.defineProperty(recorder, "recordStartupFields", { + value: function () { + recordCount++; + return oldRecord.call(recorder); + } + }); + + do_check_null(recorder._timer); + recorder.onStartup(); + do_check_eq(recordCount, 1); + do_check_eq(recorder.sessionRestored, -1); + do_check_neq(recorder._timer, null); + + yield sleep(125); + do_check_eq(recordCount, 2); + yield sleep(100); + do_check_eq(recordCount, 3); + do_check_eq(recorder.sessionRestored, -1); + + monkeypatchStartupInfo(recorder, now); + yield sleep(100); + do_check_eq(recordCount, 4); + do_check_eq(recorder.sessionRestored, 1500); + + // The timer should be removed and we should not fire again. + do_check_null(recorder._timer); + yield sleep(100); + do_check_eq(recordCount, 4); + + recorder.onShutdown(); +}); + +add_test(function test_timer_clear_on_shutdown() { + let recorder = new SessionRecorder("testing.timer_clear_on_shutdown."); + let now = new Date(); + + Object.defineProperty(recorder, "_getStartupInfo", { + value: function _getStartupInfo() { + return { + process: now, + main: new Date(now.getTime() + 500), + firstPaint: new Date(now.getTime() + 1000), + }; + }, + }); + + do_check_null(recorder._timer); + recorder.onStartup(); + do_check_neq(recorder._timer, null); + + recorder.onShutdown(); + do_check_null(recorder._timer); + + run_next_test(); +}); + +add_task(function* test_previous_clean() { + let now = new Date(); + let recorder = getRecorder("previous_clean", now); + yield sleep(25); + recorder.onStartup(); + + recorder.incrementActiveTicks(); + recorder.incrementActiveTicks(); + + yield sleep(25); + recorder.onShutdown(); + + let total = recorder.totalTime; + + yield sleep(25); + let now2 = new Date(); + let recorder2 = getRecorder("previous_clean", now2, 100); + yield sleep(25); + recorder2.onStartup(); + + do_check_eq(recorder2.startDate.getTime(), now2.getTime()); + do_check_eq(recorder2.main, 100); + do_check_eq(recorder2.firstPaint, 200); + do_check_eq(recorder2.sessionRestored, 300); + + let sessions = recorder2.getPreviousSessions(); + do_check_eq(Object.keys(sessions).length, 1); + do_check_true(0 in sessions); + let session = sessions[0]; + do_check_true(session.clean); + do_check_eq(session.startDate.getTime(), now.getTime()); + do_check_eq(session.main, 500); + do_check_eq(session.firstPaint, 1000); + do_check_eq(session.sessionRestored, 1500); + do_check_eq(session.totalTime, total); + do_check_eq(session.activeTicks, 2); + + recorder2.onShutdown(); +}); + +add_task(function* test_previous_abort() { + let now = new Date(); + let recorder = getRecorder("previous_abort", now); + yield sleep(25); + recorder.onStartup(); + recorder.incrementActiveTicks(); + yield sleep(25); + let total = recorder.totalTime; + yield sleep(25); + + let now2 = new Date(); + let recorder2 = getRecorder("previous_abort", now2); + yield sleep(25); + recorder2.onStartup(); + + let sessions = recorder2.getPreviousSessions(); + do_check_eq(Object.keys(sessions).length, 1); + do_check_true(0 in sessions); + let session = sessions[0]; + do_check_false(session.clean); + do_check_eq(session.totalTime, total); + + recorder.onShutdown(); + recorder2.onShutdown(); +}); + +add_task(function* test_multiple_sessions() { + for (let i = 0; i < 10; i++) { + let recorder = getRecorder("multiple_sessions"); + yield sleep(25); + recorder.onStartup(); + for (let j = 0; j < i; j++) { + recorder.incrementActiveTicks(); + } + yield sleep(25); + recorder.onShutdown(); + yield sleep(25); + } + + let recorder = getRecorder("multiple_sessions"); + recorder.onStartup(); + + let sessions = recorder.getPreviousSessions(); + do_check_eq(Object.keys(sessions).length, 10); + + for (let [i, session] of Object.entries(sessions)) { + do_check_eq(session.activeTicks, i); + + if (i > 0) { + do_check_true(session.startDate.getTime() > sessions[i-1].startDate.getTime()); + } + } + + // #6 is preserved since >=. + let threshold = sessions[6].startDate; + recorder.pruneOldSessions(threshold); + + sessions = recorder.getPreviousSessions(); + do_check_eq(Object.keys(sessions).length, 4); + + recorder.pruneOldSessions(threshold); + sessions = recorder.getPreviousSessions(); + do_check_eq(Object.keys(sessions).length, 4); + do_check_eq(recorder._prunedIndex, 5); + + recorder.onShutdown(); +}); + +add_task(function* test_record_activity() { + let recorder = getRecorder("record_activity"); + yield sleep(25); + recorder.onStartup(); + let total = recorder.totalTime; + yield sleep(25); + + for (let i = 0; i < 3; i++) { + Services.obs.notifyObservers(null, "user-interaction-active", null); + yield sleep(25); + do_check_true(recorder.fineTotalTime > total); + total = recorder.fineTotalTime; + } + + do_check_eq(recorder.activeTicks, 3); + + // Now send inactive. We should increment total time but not active. + Services.obs.notifyObservers(null, "user-interaction-inactive", null); + do_check_eq(recorder.activeTicks, 3); + do_check_true(recorder.fineTotalTime > total); + total = recorder.fineTotalTime; + yield sleep(25); + + // If we send active again, this should be counted as inactive. + Services.obs.notifyObservers(null, "user-interaction-active", null); + do_check_eq(recorder.activeTicks, 3); + do_check_true(recorder.fineTotalTime > total); + total = recorder.fineTotalTime; + yield sleep(25); + + // If we send active again, this should be counted as active. + Services.obs.notifyObservers(null, "user-interaction-active", null); + do_check_eq(recorder.activeTicks, 4); + + Services.obs.notifyObservers(null, "user-interaction-active", null); + do_check_eq(recorder.activeTicks, 5); + + recorder.onShutdown(); +}); + diff --git a/toolkit/modules/tests/xpcshell/test_sqlite.js b/toolkit/modules/tests/xpcshell/test_sqlite.js new file mode 100644 index 000000000..edd39d977 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_sqlite.js @@ -0,0 +1,1094 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +do_get_profile(); + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/PromiseUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Sqlite.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// To spin the event loop in test. +Cu.import("resource://services-common/async.js"); + +function sleep(ms) { + let deferred = Promise.defer(); + + let timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + + timer.initWithCallback({ + notify: function () { + deferred.resolve(); + }, + }, ms, timer.TYPE_ONE_SHOT); + + return deferred.promise; +} + +// When testing finalization, use this to tell Sqlite.jsm to not throw +// an uncatchable `Promise.reject` +function failTestsOnAutoClose(enabled) { + Cu.getGlobalForObject(Sqlite).Debugging.failTestsOnAutoClose = enabled; +} + +function getConnection(dbName, extraOptions={}) { + let path = dbName + ".sqlite"; + let options = {path: path}; + for (let [k, v] of Object.entries(extraOptions)) { + options[k] = v; + } + + return Sqlite.openConnection(options); +} + +function* getDummyDatabase(name, extraOptions={}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + yield c.execute("CREATE TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +function* getDummyTempDatabase(name, extraOptions={}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + yield c.execute("CREATE TEMP TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +function run_test() { + Cu.import("resource://testing-common/services/common/logging.js"); + initTestLogging("Trace"); + + run_next_test(); +} + +add_task(function* test_open_normal() { + let c = yield Sqlite.openConnection({path: "test_open_normal.sqlite"}); + yield c.close(); +}); + +add_task(function* test_open_unshared() { + let path = OS.Path.join(OS.Constants.Path.profileDir, "test_open_unshared.sqlite"); + + let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); + yield c.close(); +}); + +add_task(function* test_get_dummy_database() { + let db = yield getDummyDatabase("get_dummy_database"); + + do_check_eq(typeof(db), "object"); + yield db.close(); +}); + +add_task(function* test_schema_version() { + let db = yield getDummyDatabase("schema_version"); + + let version = yield db.getSchemaVersion(); + do_check_eq(version, 0); + + db.setSchemaVersion(14); + version = yield db.getSchemaVersion(); + do_check_eq(version, 14); + + for (let v of [0.5, "foobar", NaN]) { + let success; + try { + yield db.setSchemaVersion(v); + do_print("Schema version " + v + " should have been rejected"); + success = false; + } catch (ex) { + if (!ex.message.startsWith("Schema version must be an integer.")) + throw ex; + success = true; + } + do_check_true(success); + + version = yield db.getSchemaVersion(); + do_check_eq(version, 14); + } + + yield db.close(); +}); + +add_task(function* test_simple_insert() { + let c = yield getDummyDatabase("simple_insert"); + + let result = yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + do_check_true(Array.isArray(result)); + do_check_eq(result.length, 0); + yield c.close(); +}); + +add_task(function* test_simple_bound_array() { + let c = yield getDummyDatabase("simple_bound_array"); + + let result = yield c.execute("INSERT INTO dirs VALUES (?, ?)", [1, "foo"]); + do_check_eq(result.length, 0); + yield c.close(); +}); + +add_task(function* test_simple_bound_object() { + let c = yield getDummyDatabase("simple_bound_object"); + let result = yield c.execute("INSERT INTO dirs VALUES (:id, :path)", + {id: 1, path: "foo"}); + do_check_eq(result.length, 0); + result = yield c.execute("SELECT id, path FROM dirs"); + do_check_eq(result.length, 1); + do_check_eq(result[0].getResultByName("id"), 1); + do_check_eq(result[0].getResultByName("path"), "foo"); + yield c.close(); +}); + +// This is mostly a sanity test to ensure simple executions work. +add_task(function* test_simple_insert_then_select() { + let c = yield getDummyDatabase("simple_insert_then_select"); + + yield c.execute("INSERT INTO dirs VALUES (NULL, 'foo')"); + yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["bar"]); + + let result = yield c.execute("SELECT * FROM dirs"); + do_check_eq(result.length, 2); + + let i = 0; + for (let row of result) { + i++; + + do_check_eq(row.numEntries, 2); + do_check_eq(row.getResultByIndex(0), i); + + let expected = {1: "foo", 2: "bar"}[i]; + do_check_eq(row.getResultByName("path"), expected); + } + + yield c.close(); +}); + +add_task(function* test_repeat_execution() { + let c = yield getDummyDatabase("repeat_execution"); + + let sql = "INSERT INTO dirs (path) VALUES (:path)"; + yield c.executeCached(sql, {path: "foo"}); + yield c.executeCached(sql); + + let result = yield c.execute("SELECT * FROM dirs"); + + do_check_eq(result.length, 2); + + yield c.close(); +}); + +add_task(function* test_table_exists() { + let c = yield getDummyDatabase("table_exists"); + + do_check_false(yield c.tableExists("does_not_exist")); + do_check_true(yield c.tableExists("dirs")); + do_check_true(yield c.tableExists("files")); + + yield c.close(); +}); + +add_task(function* test_index_exists() { + let c = yield getDummyDatabase("index_exists"); + + do_check_false(yield c.indexExists("does_not_exist")); + + yield c.execute("CREATE INDEX my_index ON dirs (path)"); + do_check_true(yield c.indexExists("my_index")); + + yield c.close(); +}); + +add_task(function* test_temp_table_exists() { + let c = yield getDummyTempDatabase("temp_table_exists"); + + do_check_false(yield c.tableExists("temp_does_not_exist")); + do_check_true(yield c.tableExists("dirs")); + do_check_true(yield c.tableExists("files")); + + yield c.close(); +}); + +add_task(function* test_temp_index_exists() { + let c = yield getDummyTempDatabase("temp_index_exists"); + + do_check_false(yield c.indexExists("temp_does_not_exist")); + + yield c.execute("CREATE INDEX my_index ON dirs (path)"); + do_check_true(yield c.indexExists("my_index")); + + yield c.close(); +}); + +add_task(function* test_close_cached() { + let c = yield getDummyDatabase("close_cached"); + + yield c.executeCached("SELECT * FROM dirs"); + yield c.executeCached("SELECT * FROM files"); + + yield c.close(); +}); + +add_task(function* test_execute_invalid_statement() { + let c = yield getDummyDatabase("invalid_statement"); + + let deferred = Promise.defer(); + + do_check_eq(c._connectionData._anonymousStatements.size, 0); + + c.execute("SELECT invalid FROM unknown").then(do_throw, function onError(error) { + deferred.resolve(); + }); + + yield deferred.promise; + + // Ensure we don't leak the statement instance. + do_check_eq(c._connectionData._anonymousStatements.size, 0); + + yield c.close(); +}); + +add_task(function* test_incorrect_like_bindings() { + let c = yield getDummyDatabase("incorrect_like_bindings"); + + let sql = "select * from dirs where path LIKE 'non%'"; + Assert.throws(() => c.execute(sql), /Please enter a LIKE clause/); + Assert.throws(() => c.executeCached(sql), /Please enter a LIKE clause/); + + yield c.close(); +}); +add_task(function* test_on_row_exception_ignored() { + let c = yield getDummyDatabase("on_row_exception_ignored"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + yield c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = yield c.execute("SELECT * FROM DIRS", null, function onRow(row) { + i++; + + throw new Error("Some silly error."); + }); + + do_check_eq(hasResult, true); + do_check_eq(i, 10); + + yield c.close(); +}); + +// Ensure StopIteration during onRow causes processing to stop. +add_task(function* test_on_row_stop_iteration() { + let c = yield getDummyDatabase("on_row_stop_iteration"); + + let sql = "INSERT INTO dirs (path) VALUES (?)"; + for (let i = 0; i < 10; i++) { + yield c.executeCached(sql, ["dir" + i]); + } + + let i = 0; + let hasResult = yield c.execute("SELECT * FROM dirs", null, function onRow(row) { + i++; + + if (i == 5) { + throw StopIteration; + } + }); + + do_check_eq(hasResult, true); + do_check_eq(i, 5); + + yield c.close(); +}); + +// Ensure execute resolves to false when no rows are selected. +add_task(function* test_on_row_stop_iteration() { + let c = yield getDummyDatabase("no_on_row"); + + let i = 0; + let hasResult = yield c.execute(`SELECT * FROM dirs WHERE path="nonexistent"`, null, function onRow(row) { + i++; + }); + + do_check_eq(hasResult, false); + do_check_eq(i, 0); + + yield c.close(); +}); + +add_task(function* test_invalid_transaction_type() { + let c = yield getDummyDatabase("invalid_transaction_type"); + + Assert.throws(() => c.executeTransaction(function* () {}, "foobar"), + /Unknown transaction type/, + "Unknown transaction type should throw"); + + yield c.close(); +}); + +add_task(function* test_execute_transaction_success() { + let c = yield getDummyDatabase("execute_transaction_success"); + + do_check_false(c.transactionInProgress); + + yield c.executeTransaction(function* transaction(conn) { + do_check_eq(c, conn); + do_check_true(conn.transactionInProgress); + + yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + }); + + do_check_false(c.transactionInProgress); + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_true(Array.isArray(rows)); + do_check_eq(rows.length, 1); + + yield c.close(); +}); + +add_task(function* test_execute_transaction_rollback() { + let c = yield getDummyDatabase("execute_transaction_rollback"); + + let deferred = Promise.defer(); + + c.executeTransaction(function* transaction(conn) { + yield conn.execute("INSERT INTO dirs (path) VALUES ('foo')"); + print("Expecting error with next statement."); + yield conn.execute("INSERT INTO invalid VALUES ('foo')"); + + // We should never get here. + do_throw(); + }).then(do_throw, function onError(error) { + deferred.resolve(); + }); + + yield deferred.promise; + + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 0); + + yield c.close(); +}); + +add_task(function* test_close_during_transaction() { + let c = yield getDummyDatabase("close_during_transaction"); + + yield c.execute("INSERT INTO dirs (path) VALUES ('foo')"); + + let promise = c.executeTransaction(function* transaction(conn) { + yield c.execute("INSERT INTO dirs (path) VALUES ('bar')"); + }); + yield c.close(); + + yield Assert.rejects(promise, + /Transaction canceled due to a closed connection/, + "closing a connection in the middle of a transaction should reject it"); + + let c2 = yield getConnection("close_during_transaction"); + let rows = yield c2.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 1); + + yield c2.close(); +}); + +// Verify that we support concurrent transactions. +add_task(function* test_multiple_transactions() { + let c = yield getDummyDatabase("detect_multiple_transactions"); + + for (let i = 0; i < 10; ++i) { + // We don't wait for these transactions. + c.executeTransaction(function* () { + yield c.execute("INSERT INTO dirs (path) VALUES (:path)", + { path: `foo${i}` }); + yield c.execute("SELECT * FROM dirs"); + }); + } + for (let i = 0; i < 10; ++i) { + yield c.executeTransaction(function* () { + yield c.execute("INSERT INTO dirs (path) VALUES (:path)", + { path: `bar${i}` }); + yield c.execute("SELECT * FROM dirs"); + }); + } + + let rows = yield c.execute("SELECT * FROM dirs"); + do_check_eq(rows.length, 20); + + yield c.close(); +}); + +// Verify that wrapped transactions ignore a BEGIN TRANSACTION failure, when +// an externally opened transaction exists. +add_task(function* test_wrapped_connection_transaction() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_wrapStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let wrapper = yield Sqlite.wrapStorageConnection({ connection: c }); + // Start a transaction on the raw connection. + yield c.executeSimpleSQLAsync("BEGIN"); + // Now use executeTransaction, it will be executed, but not in a transaction. + yield wrapper.executeTransaction(function* () { + yield wrapper.execute("CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT)"); + }); + // This should not fail cause the internal transaction has not been created. + yield c.executeSimpleSQLAsync("COMMIT"); + + yield wrapper.execute("SELECT * FROM test"); + + // Closing the wrapper should just finalize statements but not close the + // database. + yield wrapper.close(); + yield c.asyncClose(); +}); + +add_task(function* test_shrink_memory() { + let c = yield getDummyDatabase("shrink_memory"); + + // It's just a simple sanity test. We have no way of measuring whether this + // actually does anything. + + yield c.shrinkMemory(); + yield c.close(); +}); + +add_task(function* test_no_shrink_on_init() { + let c = yield getConnection("no_shrink_on_init", + {shrinkMemoryOnConnectionIdleMS: 200}); + + let oldShrink = c._connectionData.shrinkMemory; + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + }, + }); + + // We should not shrink until a statement has been executed. + yield sleep(220); + do_check_eq(count, 0); + + yield c.execute("SELECT 1"); + yield sleep(220); + do_check_eq(count, 1); + + yield c.close(); +}); + +add_task(function* test_idle_shrink_fires() { + let c = yield getDummyDatabase("idle_shrink_fires", + {shrinkMemoryOnConnectionIdleMS: 200}); + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + + let count = 0; + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + // We reset the idle shrink timer after monkeypatching because otherwise the + // installed timer callback will reference the non-monkeypatched function. + c._connectionData._startIdleShrinkTimer(); + + yield sleep(220); + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + shrinkPromises.shift(); + + // We shouldn't shrink again unless a statement was executed. + yield sleep(300); + do_check_eq(count, 1); + + yield c.execute("SELECT 1"); + yield sleep(300); + + do_check_eq(count, 2); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function* test_idle_shrink_reset_on_operation() { + const INTERVAL = 500; + let c = yield getDummyDatabase("idle_shrink_reset_on_operation", + {shrinkMemoryOnConnectionIdleMS: INTERVAL}); + + c._connectionData._clearIdleShrinkTimer(); + + let oldShrink = c._connectionData.shrinkMemory; + let shrinkPromises = []; + let count = 0; + + Object.defineProperty(c._connectionData, "shrinkMemory", { + value: function () { + count++; + let promise = oldShrink.call(c._connectionData); + shrinkPromises.push(promise); + return promise; + }, + }); + + let now = new Date(); + c._connectionData._startIdleShrinkTimer(); + + let initialIdle = new Date(now.getTime() + INTERVAL); + + // Perform database operations until initial scheduled time has been passed. + let i = 0; + while (new Date() < initialIdle) { + yield c.execute("INSERT INTO dirs (path) VALUES (?)", ["" + i]); + i++; + } + + do_check_true(i > 0); + + // We should not have performed an idle while doing operations. + do_check_eq(count, 0); + + // Wait for idle timer. + yield sleep(INTERVAL); + + // Ensure we fired. + do_check_eq(count, 1); + do_check_eq(shrinkPromises.length, 1); + yield shrinkPromises[0]; + + yield c.close(); +}); + +add_task(function* test_in_progress_counts() { + let c = yield getDummyDatabase("in_progress_counts"); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount); + do_check_eq(c._connectionData._pendingStatements.size, 0); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount + 1); + do_check_eq(c._connectionData._pendingStatements.size, 0); + + let expectOne; + let expectTwo; + + // Please forgive me. + let inner = Async.makeSpinningCallback(); + let outer = Async.makeSpinningCallback(); + + // We want to make sure that two queries executing simultaneously + // result in `_pendingStatements.size` reaching 2, then dropping back to 0. + // + // To do so, we kick off a second statement within the row handler + // of the first, then wait for both to finish. + + yield c.executeCached("SELECT * from dirs", null, function onRow() { + // In the onRow handler, we're still an outstanding query. + // Expect a single in-progress entry. + expectOne = c._connectionData._pendingStatements.size; + + // Start another query, checking that after its statement has been created + // there are two statements in progress. + let p = c.executeCached("SELECT 10, path from dirs"); + expectTwo = c._connectionData._pendingStatements.size; + + // Now wait for it to be done before we return from the row handler … + p.then(function onInner() { + inner(); + }); + }).then(function onOuter() { + // … and wait for the inner to be done before we finish … + inner.wait(); + outer(); + }); + + // … and wait for both queries to have finished before we go on and + // test postconditions. + outer.wait(); + + do_check_eq(expectOne, 1); + do_check_eq(expectTwo, 2); + do_check_eq(c._connectionData._statementCounter, c._initialStatementCount + 3); + do_check_eq(c._connectionData._pendingStatements.size, 0); + + yield c.close(); +}); + +add_task(function* test_discard_while_active() { + let c = yield getDummyDatabase("discard_while_active"); + + yield c.executeCached("INSERT INTO dirs (path) VALUES ('foo')"); + yield c.executeCached("INSERT INTO dirs (path) VALUES ('bar')"); + + let discarded = -1; + let first = true; + let sql = "SELECT * FROM dirs"; + yield c.executeCached(sql, null, function onRow(row) { + if (!first) { + return; + } + first = false; + discarded = c.discardCachedStatements(); + }); + + // We discarded everything, because the SELECT had already started to run. + do_check_eq(3, discarded); + + // And again is safe. + do_check_eq(0, c.discardCachedStatements()); + + yield c.close(); +}); + +add_task(function* test_discard_cached() { + let c = yield getDummyDatabase("discard_cached"); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(1, c._connectionData._cachedStatements.size); + + yield c.executeCached("SELECT * from files"); + do_check_eq(2, c._connectionData._cachedStatements.size); + + yield c.executeCached("SELECT * from dirs"); + do_check_eq(2, c._connectionData._cachedStatements.size); + + c.discardCachedStatements(); + do_check_eq(0, c._connectionData._cachedStatements.size); + + yield c.close(); +}); + +add_task(function* test_programmatic_binding() { + let c = yield getDummyDatabase("programmatic_binding"); + + let bindings = [ + {id: 1, path: "foobar"}, + {id: null, path: "baznoo"}, + {id: 5, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let result = yield c.execute(sql, bindings); + do_check_eq(result.length, 0); + + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + yield c.close(); +}); + +add_task(function* test_programmatic_binding_transaction() { + let c = yield getDummyDatabase("programmatic_binding_transaction"); + + let bindings = [ + {id: 1, path: "foobar"}, + {id: null, path: "baznoo"}, + {id: 5, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + yield c.executeTransaction(function* transaction() { + let result = yield c.execute(sql, bindings); + do_check_eq(result.length, 0); + + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + }); + + // Transaction committed. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 3); + yield c.close(); +}); + +add_task(function* test_programmatic_binding_transaction_partial_rollback() { + let c = yield getDummyDatabase("programmatic_binding_transaction_partial_rollback"); + + let bindings = [ + {id: 2, path: "foobar"}, + {id: 3, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + + // Add some data in an implicit transaction before beginning the batch insert. + yield c.execute(sql, {id: 1, path: "works"}); + + let secondSucceeded = false; + try { + yield c.executeTransaction(function* transaction() { + // Insert one row. This won't implicitly start a transaction. + let result = yield c.execute(sql, bindings[0]); + + // Insert multiple rows. mozStorage will want to start a transaction. + // One of the inserts will fail, so the transaction should be rolled back. + result = yield c.execute(sql, bindings); + secondSucceeded = true; + }); + } catch (ex) { + print("Caught expected exception: " + ex); + } + + // We did not get to the end of our in-transaction block. + do_check_false(secondSucceeded); + + // Everything that happened in *our* transaction, not mozStorage's, got + // rolled back, but the first row still exists. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 1); + do_check_eq(rows[0].getResultByName("path"), "works"); + yield c.close(); +}); + +// Just like the previous test, but relying on the implicit +// transaction established by mozStorage. +add_task(function* test_programmatic_binding_implicit_transaction() { + let c = yield getDummyDatabase("programmatic_binding_implicit_transaction"); + + let bindings = [ + {id: 2, path: "foobar"}, + {id: 1, path: "toofoo"}, + ]; + + let sql = "INSERT INTO dirs VALUES (:id, :path)"; + let secondSucceeded = false; + yield c.execute(sql, {id: 1, path: "works"}); + try { + let result = yield c.execute(sql, bindings); + secondSucceeded = true; + } catch (ex) { + print("Caught expected exception: " + ex); + } + + do_check_false(secondSucceeded); + + // The entire batch failed. + let rows = yield c.executeCached("SELECT * from dirs"); + do_check_eq(rows.length, 1); + do_check_eq(rows[0].getResultByName("path"), "works"); + yield c.close(); +}); + +// Test that direct binding of params and execution through mozStorage doesn't +// error when we manually create a transaction. See Bug 856925. +add_task(function* test_direct() { + let file = FileUtils.getFile("TmpD", ["test_direct.sqlite"]); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + print("Opening " + file.path); + + let db = Services.storage.openDatabase(file); + print("Opened " + db); + + db.executeSimpleSQL("CREATE TABLE types (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, UNIQUE (name))"); + print("Executed setup."); + + let statement = db.createAsyncStatement("INSERT INTO types (name) VALUES (:name)"); + let params = statement.newBindingParamsArray(); + let one = params.newBindingParams(); + one.bindByName("name", null); + params.addParams(one); + let two = params.newBindingParams(); + two.bindByName("name", "bar"); + params.addParams(two); + + print("Beginning transaction."); + let begin = db.createAsyncStatement("BEGIN DEFERRED TRANSACTION"); + let end = db.createAsyncStatement("COMMIT TRANSACTION"); + + let deferred = Promise.defer(); + begin.executeAsync({ + handleCompletion: function (reason) { + deferred.resolve(); + } + }); + yield deferred.promise; + + statement.bindParameters(params); + + deferred = Promise.defer(); + print("Executing async."); + statement.executeAsync({ + handleResult: function (resultSet) { + }, + + handleError: function (error) { + print("Error when executing SQL (" + error.result + "): " + + error.message); + print("Original error: " + error.error); + errors.push(error); + deferred.reject(); + }, + + handleCompletion: function (reason) { + print("Completed."); + deferred.resolve(); + } + }); + + yield deferred.promise; + + deferred = Promise.defer(); + end.executeAsync({ + handleCompletion: function (reason) { + deferred.resolve(); + } + }); + yield deferred.promise; + + statement.finalize(); + begin.finalize(); + end.finalize(); + + deferred = Promise.defer(); + db.asyncClose(function () { + deferred.resolve() + }); + yield deferred.promise; +}); + +// Test Sqlite.cloneStorageConnection. +add_task(function* test_cloneStorageConnection() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_cloneStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let clone = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: true }); + // Just check that it works. + yield clone.execute("SELECT 1"); + + let clone2 = yield Sqlite.cloneStorageConnection({ connection: c, readOnly: false }); + // Just check that it works. + yield clone2.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + + // Closing order should not matter. + yield c.asyncClose(); + yield clone2.close(); + yield clone.close(); +}); + +// Test Sqlite.cloneStorageConnection invalid argument. +add_task(function* test_cloneStorageConnection() { + try { + let clone = yield Sqlite.cloneStorageConnection({ connection: null }); + do_throw(new Error("Should throw on invalid connection")); + } catch (ex) { + if (ex.name != "TypeError") { + throw ex; + } + } +}); + +// Test clone() method. +add_task(function* test_clone() { + let c = yield getDummyDatabase("clone"); + + let clone = yield c.clone(); + // Just check that it works. + yield clone.execute("SELECT 1"); + // Closing order should not matter. + yield c.close(); + yield clone.close(); +}); + +// Test clone(readOnly) method. +add_task(function* test_readOnly_clone() { + let path = OS.Path.join(OS.Constants.Path.profileDir, "test_readOnly_clone.sqlite"); + let c = yield Sqlite.openConnection({path: path, sharedMemoryCache: false}); + + let clone = yield c.clone(true); + // Just check that it works. + yield clone.execute("SELECT 1"); + // But should not be able to write. + + yield Assert.rejects(clone.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"), + /readonly/); + // Closing order should not matter. + yield c.close(); + yield clone.close(); +}); + +// Test Sqlite.wrapStorageConnection. +add_task(function* test_wrapStorageConnection() { + let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir, + "test_wrapStorageConnection.sqlite")); + let c = yield new Promise((resolve, reject) => { + Services.storage.openAsyncDatabase(file, null, (status, db) => { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(new Error(status)); + } + }); + }); + + let wrapper = yield Sqlite.wrapStorageConnection({ connection: c }); + // Just check that it works. + yield wrapper.execute("SELECT 1"); + yield wrapper.executeCached("SELECT 1"); + + // Closing the wrapper should just finalize statements but not close the + // database. + yield wrapper.close(); + yield c.asyncClose(); +}); + +// Test finalization +add_task(function* test_closed_by_witness() { + failTestsOnAutoClose(false); + let c = yield getDummyDatabase("closed_by_witness"); + + Services.obs.notifyObservers(null, "sqlite-finalization-witness", + c._connectionData._identifier); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + yield c._connectionData._deferredClose.promise; + do_check_false(c._connectionData._open); + failTestsOnAutoClose(true); +}); + +add_task(function* test_warning_message_on_finalization() { + failTestsOnAutoClose(false); + let c = yield getDummyDatabase("warning_message_on_finalization"); + let identifier = c._connectionData._identifier; + let deferred = Promise.defer(); + + let listener = { + observe: function(msg) { + let messageText = msg.message; + // Make sure the message starts with a warning containing the + // connection identifier + if (messageText.indexOf("Warning: Sqlite connection '" + identifier + "'") !== -1) { + deferred.resolve(); + } + } + }; + Services.console.registerListener(listener); + + Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier); + // Since we triggered finalization ourselves, tell the witness to + // forget the connection so it does not trigger a finalization again + c._witness.forget(); + + yield deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(function* test_error_message_on_unknown_finalization() { + failTestsOnAutoClose(false); + let deferred = Promise.defer(); + + let listener = { + observe: function(msg) { + let messageText = msg.message; + if (messageText.indexOf("Error: Attempt to finalize unknown " + + "Sqlite connection: foo") !== -1) { + deferred.resolve(); + } + } + }; + Services.console.registerListener(listener); + Services.obs.notifyObservers(null, "sqlite-finalization-witness", "foo"); + + yield deferred.promise; + Services.console.unregisterListener(listener); + failTestsOnAutoClose(true); +}); + +add_task(function* test_forget_witness_on_close() { + let c = yield getDummyDatabase("forget_witness_on_close"); + + let forgetCalled = false; + let oldWitness = c._witness; + c._witness = { + forget: function () { + forgetCalled = true; + oldWitness.forget(); + }, + }; + + yield c.close(); + // After close, witness should have forgotten the connection + do_check_true(forgetCalled); +}); + +add_task(function* test_close_database_on_gc() { + failTestsOnAutoClose(false); + let finalPromise; + + { + let collectedPromises = []; + for (let i = 0; i < 100; ++i) { + let deferred = PromiseUtils.defer(); + let c = yield getDummyDatabase("gc_" + i); + c._connectionData._deferredClose.promise.then(deferred.resolve); + collectedPromises.push(deferred.promise); + } + finalPromise = Promise.all(collectedPromises); + } + + // Call getDummyDatabase once more to clear any remaining + // references. This is needed at the moment, otherwise + // garbage-collection takes place after the shutdown barrier and the + // test will timeout. Once that is fixed, we can remove this line + // and be fine as long as the connections are garbage-collected. + let last = yield getDummyDatabase("gc_last"); + yield last.close(); + + Components.utils.forceGC(); + Components.utils.forceCC(); + Components.utils.forceShrinkingGC(); + + yield finalPromise; + failTestsOnAutoClose(true); +}); diff --git a/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js b/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js new file mode 100644 index 000000000..b97fd8558 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_sqlite_shutdown.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +do_get_profile(); + +Cu.import("resource://gre/modules/osfile.jsm"); + // OS.File doesn't like to be first imported during shutdown +Cu.import("resource://gre/modules/Sqlite.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AsyncShutdown.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +function getConnection(dbName, extraOptions={}) { + let path = dbName + ".sqlite"; + let options = {path: path}; + for (let [k, v] of Object.entries(extraOptions)) { + options[k] = v; + } + + return Sqlite.openConnection(options); +} + +function* getDummyDatabase(name, extraOptions={}) { + const TABLES = { + dirs: "id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT", + files: "id INTEGER PRIMARY KEY AUTOINCREMENT, dir_id INTEGER, path TEXT", + }; + + let c = yield getConnection(name, extraOptions); + c._initialStatementCount = 0; + + for (let [k, v] of Object.entries(TABLES)) { + yield c.execute("CREATE TABLE " + k + "(" + v + ")"); + c._initialStatementCount++; + } + + return c; +} + +function sleep(ms) { + let deferred = Promise.defer(); + + let timer = Cc["@mozilla.org/timer;1"] + .createInstance(Ci.nsITimer); + + timer.initWithCallback({ + notify: function () { + deferred.resolve(); + }, + }, ms, timer.TYPE_ONE_SHOT); + + return deferred.promise; +} + +function run_test() { + run_next_test(); +} + + +// +// ----------- Don't add a test after this one, as it shuts down Sqlite.jsm +// +add_task(function* test_shutdown_clients() { + do_print("Ensuring that Sqlite.jsm doesn't shutdown before its clients"); + + let assertions = []; + + let sleepStarted = false; + let sleepComplete = false; + Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (sleep)", + Task.async(function*() { + sleepStarted = true; + yield sleep(100); + sleepComplete = true; + })); + assertions.push({name: "sleepStarted", value: () => sleepStarted}); + assertions.push({name: "sleepComplete", value: () => sleepComplete}); + + Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (immediate)", + true); + + let dbOpened = false; + let dbClosed = false; + + Sqlite.shutdown.addBlocker("test_sqlite.js shutdown blocker (open a connection during shutdown)", + Task.async(function*() { + let db = yield getDummyDatabase("opened during shutdown"); + dbOpened = true; + db.close().then( + () => dbClosed = true + ); // Don't wait for this task to complete, Sqlite.jsm must wait automatically + })); + + assertions.push({name: "dbOpened", value: () => dbOpened}); + assertions.push({name: "dbClosed", value: () => dbClosed}); + + do_print("Now shutdown Sqlite.jsm synchronously"); + Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); + AsyncShutdown.profileBeforeChange._trigger(); + Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); + + + for (let {name, value} of assertions) { + do_print("Checking: " + name); + do_check_true(value()); + } + + do_print("Ensure that we cannot open databases anymore"); + let exn; + try { + yield getDummyDatabase("opened after shutdown"); + } catch (ex) { + exn = ex; + } + do_check_true(!!exn); + do_check_true(exn.message.indexOf("Sqlite.jsm has been shutdown") != -1); +}); diff --git a/toolkit/modules/tests/xpcshell/test_task.js b/toolkit/modules/tests/xpcshell/test_task.js new file mode 100644 index 000000000..fdcd56514 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_task.js @@ -0,0 +1,642 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the Task.jsm module. + */ + +//////////////////////////////////////////////////////////////////////////////// +/// Globals + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", + "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +/** + * Returns a promise that will be resolved with the given value, when an event + * posted on the event loop of the main thread is processed. + */ +function promiseResolvedLater(aValue) { + let deferred = Promise.defer(); + Services.tm.mainThread.dispatch(() => deferred.resolve(aValue), + Ci.nsIThread.DISPATCH_NORMAL); + return deferred.promise; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Tests + +function run_test() +{ + run_next_test(); +} + +add_test(function test_normal() +{ + Task.spawn(function () { + let result = yield Promise.resolve("Value"); + for (let i = 0; i < 3; i++) { + result += yield promiseResolvedLater("!"); + } + throw new Task.Result("Task result: " + result); + }).then(function (result) { + do_check_eq("Task result: Value!!!", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_exceptions() +{ + Task.spawn(function () { + try { + yield Promise.reject("Rejection result by promise."); + do_throw("Exception expected because the promise was rejected."); + } catch (ex) { + // We catch this exception now, we will throw a different one later. + do_check_eq("Rejection result by promise.", ex); + } + throw new Error("Exception uncaught by task."); + }).then(function (result) { + do_throw("Unexpected success!"); + }, function (ex) { + do_check_eq("Exception uncaught by task.", ex.message); + run_next_test(); + }); +}); + +add_test(function test_recursion() +{ + function task_fibonacci(n) { + throw new Task.Result(n < 2 ? n : (yield task_fibonacci(n - 1)) + + (yield task_fibonacci(n - 2))); + }; + + Task.spawn(task_fibonacci(6)).then(function (result) { + do_check_eq(8, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_primitive() +{ + function fibonacci(n) { + return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); + }; + + // Polymorphism between task and non-task functions (see "test_recursion"). + Task.spawn(fibonacci(6)).then(function (result) { + do_check_eq(8, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function() +{ + Task.spawn(function () { + return "This is not a generator."; + }).then(function (result) { + do_check_eq("This is not a generator.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_this() +{ + Task.spawn(function () { + return this; + }).then(function (result) { + // Since the task function wasn't defined in strict mode, its "this" object + // should be the same as the "this" object in this function, i.e. the global + // object. + do_check_eq(result, this); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_this_strict() +{ + "use strict"; + Task.spawn(function () { + return this; + }).then(function (result) { + // Since the task function was defined in strict mode, its "this" object + // should be undefined. + do_check_eq(typeof(result), "undefined"); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_returning_promise() +{ + Task.spawn(function () { + return promiseResolvedLater("Resolution value."); + }).then(function (result) { + do_check_eq("Resolution value.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_spawn_function_exceptions() +{ + Task.spawn(function () { + throw new Error("Exception uncaught by task."); + }).then(function (result) { + do_throw("Unexpected success!"); + }, function (ex) { + do_check_eq("Exception uncaught by task.", ex.message); + run_next_test(); + }); +}); + +add_test(function test_spawn_function_taskresult() +{ + Task.spawn(function () { + throw new Task.Result("Task result"); + }).then(function (result) { + do_check_eq("Task result", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_yielded_undefined() +{ + Task.spawn(function () { + yield; + throw new Task.Result("We continued correctly."); + }).then(function (result) { + do_check_eq("We continued correctly.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_yielded_primitive() +{ + Task.spawn(function () { + throw new Task.Result("Primitive " + (yield "value.")); + }).then(function (result) { + do_check_eq("Primitive value.", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_star_normal() +{ + Task.spawn(function* () { + let result = yield Promise.resolve("Value"); + for (let i = 0; i < 3; i++) { + result += yield promiseResolvedLater("!"); + } + return "Task result: " + result; + }).then(function (result) { + do_check_eq("Task result: Value!!!", result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_star_exceptions() +{ + Task.spawn(function* () { + try { + yield Promise.reject("Rejection result by promise."); + do_throw("Exception expected because the promise was rejected."); + } catch (ex) { + // We catch this exception now, we will throw a different one later. + do_check_eq("Rejection result by promise.", ex); + } + throw new Error("Exception uncaught by task."); + }).then(function (result) { + do_throw("Unexpected success!"); + }, function (ex) { + do_check_eq("Exception uncaught by task.", ex.message); + run_next_test(); + }); +}); + +add_test(function test_star_recursion() +{ + function* task_fibonacci(n) { + return n < 2 ? n : (yield task_fibonacci(n - 1)) + + (yield task_fibonacci(n - 2)); + }; + + Task.spawn(task_fibonacci(6)).then(function (result) { + do_check_eq(8, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_mixed_legacy_and_star() +{ + Task.spawn(function* () { + return yield (function() { + throw new Task.Result(yield 5); + })(); + }).then(function (result) { + do_check_eq(5, result); + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_from_generator() +{ + Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function* (param) { + do_check_eq(this, object); + return param; + }) + }; + + // Ensure the async function returns a promise that resolves as expected. + do_check_eq((yield object.asyncFunction(1)), 1); + + // Ensure a second call to the async function also returns such a promise. + do_check_eq((yield object.asyncFunction(3)), 3); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_from_function() +{ + Task.spawn(function* () { + return Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function (param) { + do_check_eq(this, object); + return param; + }) + }; + + // Ensure the async function returns a promise that resolves as expected. + do_check_eq((yield object.asyncFunction(5)), 5); + + // Ensure a second call to the async function also returns such a promise. + do_check_eq((yield object.asyncFunction(7)), 7); + }); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_function_that_throws_rejects_promise() +{ + Task.spawn(function* () { + let object = { + asyncFunction: Task.async(function* () { + throw "Rejected!"; + }) + }; + + yield object.asyncFunction(); + }).then(function () { + do_throw("unexpected success calling async function that throws error"); + }, function (ex) { + do_check_eq(ex, "Rejected!"); + run_next_test(); + }); +}); + +add_test(function test_async_return_function() +{ + Task.spawn(function* () { + // Ensure an async function that returns a function resolves to the function + // itself instead of calling the function and resolving to its return value. + return Task.spawn(function* () { + let returnValue = function () { + return "These aren't the droids you're looking for."; + }; + + let asyncFunction = Task.async(function () { + return returnValue; + }); + + do_check_eq((yield asyncFunction()), returnValue); + }); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_throw_argument_not_function() +{ + Task.spawn(function* () { + // Ensure Task.async throws if its aTask argument is not a function. + Assert.throws(() => Task.async("not a function"), + /aTask argument must be a function/); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + +add_test(function test_async_throw_on_function_in_place_of_promise() +{ + Task.spawn(function* () { + // Ensure Task.spawn throws if passed an async function. + Assert.throws(() => Task.spawn(Task.async(function* () {})), + /Cannot use an async function in place of a promise/); + }).then(function () { + run_next_test(); + }, function (ex) { + do_throw("Unexpected error: " + ex); + }); +}); + + +////////////////// Test rewriting of stack traces + +// Backup Task.Debuggin.maintainStack. +// Will be restored by `exit_stack_tests`. +var maintainStack; +add_test(function enter_stack_tests() { + maintainStack = Task.Debugging.maintainStack; + Task.Debugging.maintainStack = true; + run_next_test(); +}); + + +/** + * Ensure that a list of frames appear in a stack, in the right order + */ +function do_check_rewritten_stack(frames, ex) { + do_print("Checking that the expected frames appear in the right order"); + do_print(frames.join(", ")); + let stack = ex.stack; + do_print(stack); + + let framesFound = 0; + let lineNumber = 0; + let reLine = /([^\r\n])+/g; + let match; + while (framesFound < frames.length && (match = reLine.exec(stack))) { + let line = match[0]; + let frame = frames[framesFound]; + do_print("Searching for " + frame + " in line " + line); + if (line.indexOf(frame) != -1) { + do_print("Found " + frame); + ++framesFound; + } else { + do_print("Didn't find " + frame); + } + } + + if (framesFound >= frames.length) { + return; + } + do_throw("Did not find: " + frames.slice(framesFound).join(", ") + + " in " + stack.substr(reLine.lastIndex)); + + do_print("Ensuring that we have removed Task.jsm, Promise.jsm"); + do_check_true(stack.indexOf("Task.jsm") == -1); + do_check_true(stack.indexOf("Promise.jsm") == -1); + do_check_true(stack.indexOf("Promise-backend.js") == -1); +} + + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.spawn. +add_test(function test_spawn_throw_stack() { + Task.spawn(function* task_spawn_throw_stack() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_spawn_throw_stack", + "test_spawn_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we yield +// a rejection in a Task.spawn. +add_test(function test_spawn_yield_reject_stack() { + Task.spawn(function* task_spawn_yield_reject_stack() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_spawn_yield_reject_stack", + "test_spawn_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_function_throw_stack() { + let task_async_function_throw_stack = Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + })().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_function_throw_stack", + "test_async_function_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_function_yield_reject_stack() { + let task_async_function_yield_reject_stack = Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + })().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_function_yield_reject_stack", + "test_async_function_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_method_throw_stack() { + let object = { + task_async_method_throw_stack: Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + throw new Error("BOOM"); + }) + }; + object.task_async_method_throw_stack().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_method_throw_stack", + "test_async_method_throw_stack"], + ex); + run_next_test(); + }); +}); + +// Test that we get an acceptable rewritten stack when we launch +// an error in a Task.async function. +add_test(function test_async_method_yield_reject_stack() { + let object = { + task_async_method_yield_reject_stack: Task.async(function*() { + for (let i = 0; i < 5; ++i) { + yield Promise.resolve(); // Without stack rewrite, this would lose valuable information + } + yield Promise.reject(new Error("BOOM")); + }) + }; + object.task_async_method_yield_reject_stack().then(do_throw, function(ex) { + do_check_rewritten_stack(["task_async_method_yield_reject_stack", + "test_async_method_yield_reject_stack"], + ex); + run_next_test(); + }); +}); + +// Test that two tasks whose execution takes place interleaved do not capture each other's stack. +add_test(function test_throw_stack_do_not_capture_the_wrong_task() { + for (let iter_a of [3, 4, 5]) { // Vary the interleaving + for (let iter_b of [3, 4, 5]) { + Task.spawn(function* task_a() { + for (let i = 0; i < iter_a; ++i) { + yield Promise.resolve(); + } + throw new Error("BOOM"); + }).then(do_throw, function(ex) { + do_check_rewritten_stack(["task_a", + "test_throw_stack_do_not_capture_the_wrong_task"], + ex); + do_check_true(!ex.stack.includes("task_b")); + run_next_test(); + }); + Task.spawn(function* task_b() { + for (let i = 0; i < iter_b; ++i) { + yield Promise.resolve(); + } + }); + } + } +}); + +// Put things together +add_test(function test_throw_complex_stack() +{ + // Setup the following stack: + // inner_method() + // task_3() + // task_2() + // task_1() + // function_3() + // function_2() + // function_1() + // test_throw_complex_stack() + (function function_1() { + return (function function_2() { + return (function function_3() { + return Task.spawn(function* task_1() { + yield Promise.resolve(); + try { + yield Task.spawn(function* task_2() { + yield Promise.resolve(); + yield Task.spawn(function* task_3() { + yield Promise.resolve(); + let inner_object = { + inner_method: Task.async(function*() { + throw new Error("BOOM"); + }) + }; + yield Promise.resolve(); + yield inner_object.inner_method(); + }); + }); + } catch (ex) { + yield Promise.resolve(); + throw ex; + } + }); + })(); + })(); + })().then( + () => do_throw("Shouldn't have succeeded"), + (ex) => { + let expect = ["inner_method", + "task_3", + "task_2", + "task_1", + "function_3", + "function_2", + "function_1", + "test_throw_complex_stack"]; + do_check_rewritten_stack(expect, ex); + + run_next_test(); + }); +}); + +add_test(function test_without_maintainStack() { + do_print("Calling generateReadableStack without a Task"); + Task.Debugging.generateReadableStack(new Error("Not a real error")); + + Task.Debugging.maintainStack = false; + + do_print("Calling generateReadableStack with neither a Task nor maintainStack"); + Task.Debugging.generateReadableStack(new Error("Not a real error")); + + do_print("Calling generateReadableStack without maintainStack"); + Task.spawn(function*() { + Task.Debugging.generateReadableStack(new Error("Not a real error")); + run_next_test(); + }); +}); + +add_test(function exit_stack_tests() { + Task.Debugging.maintainStack = maintainStack; + run_next_test(); +}); + diff --git a/toolkit/modules/tests/xpcshell/test_timer.js b/toolkit/modules/tests/xpcshell/test_timer.js new file mode 100644 index 000000000..57e300663 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_timer.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests exports from Timer.jsm + +var imported = {}; +Components.utils.import("resource://gre/modules/Timer.jsm", imported); + +function run_test() { + run_next_test(); +} + +add_task(function* test_setTimeout() { + let timeout1 = imported.setTimeout(() => do_throw("Should not be called"), 100); + do_check_eq(typeof timeout1, "number", "setTimeout returns a number"); + do_check_true(timeout1 > 0, "setTimeout returns a positive number"); + + imported.clearTimeout(timeout1); + + yield new Promise((resolve) => { + let timeout2 = imported.setTimeout((param1, param2) => { + do_check_true(true, "Should be called"); + do_check_eq(param1, 5, "first parameter is correct"); + do_check_eq(param2, "test", "second parameter is correct"); + resolve(); + }, 100, 5, "test"); + + do_check_eq(typeof timeout2, "number", "setTimeout returns a number"); + do_check_true(timeout2 > 0, "setTimeout returns a positive number"); + do_check_neq(timeout1, timeout2, "Calling setTimeout again returns a different value"); + }); +}); + +add_task(function* test_setInterval() { + let interval1 = imported.setInterval(() => do_throw("Should not be called!"), 100); + do_check_eq(typeof interval1, "number", "setInterval returns a number"); + do_check_true(interval1 > 0, "setTimeout returns a positive number"); + + imported.clearInterval(interval1); + + const EXPECTED_CALLS = 5; + let calls = 0; + + yield new Promise((resolve) => { + let interval2 = imported.setInterval((param1, param2) => { + do_check_true(true, "Should be called"); + do_check_eq(param1, 15, "first parameter is correct"); + do_check_eq(param2, "hola", "second parameter is correct"); + if (calls >= EXPECTED_CALLS) { + resolve(); + } + calls++; + }, 100, 15, "hola"); + }); +}); diff --git a/toolkit/modules/tests/xpcshell/test_web_channel.js b/toolkit/modules/tests/xpcshell/test_web_channel.js new file mode 100644 index 000000000..05f1bc03d --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_web_channel.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/WebChannel.jsm"); + +const ERROR_ID_ORIGIN_REQUIRED = "WebChannel id and originOrPermission are required."; +const VALID_WEB_CHANNEL_ID = "id"; +const URL_STRING = "http://example.com"; +const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null); +const TEST_PERMISSION_NAME = "test-webchannel-permissions"; + +var MockWebChannelBroker = { + _channelMap: new Map(), + registerChannel: function(channel) { + if (!this._channelMap.has(channel)) { + this._channelMap.set(channel); + } + }, + unregisterChannel: function (channelToRemove) { + this._channelMap.delete(channelToRemove) + } +}; + +function run_test() { + run_next_test(); +} + +/** + * Web channel tests + */ + +/** + * Test channel listening with originOrPermission being an nsIURI. + */ +add_task(function test_web_channel_listen() { + return new Promise((resolve, reject) => { + let channel = new WebChannel(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN, { + broker: MockWebChannelBroker + }); + let delivered = 0; + do_check_eq(channel.id, VALID_WEB_CHANNEL_ID); + do_check_eq(channel._originOrPermission.spec, VALID_WEB_CHANNEL_ORIGIN.spec); + do_check_eq(channel._deliverCallback, null); + + channel.listen(function(id, message, target) { + do_check_eq(id, VALID_WEB_CHANNEL_ID); + do_check_true(message); + do_check_true(message.command); + do_check_true(target.sender); + delivered++; + // 2 messages should be delivered + if (delivered === 2) { + channel.stopListening(); + do_check_eq(channel._deliverCallback, null); + resolve(); + } + }); + + // send two messages + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "one" + } + }, { sender: true }); + + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "two" + } + }, { sender: true }); + }); +}); + +/** + * Test channel listening with originOrPermission being a permission string. + */ +add_task(function test_web_channel_listen_permission() { + return new Promise((resolve, reject) => { + // add a new permission + Services.perms.add(VALID_WEB_CHANNEL_ORIGIN, TEST_PERMISSION_NAME, Services.perms.ALLOW_ACTION); + do_register_cleanup(() => Services.perms.remove(VALID_WEB_CHANNEL_ORIGIN, TEST_PERMISSION_NAME)); + let channel = new WebChannel(VALID_WEB_CHANNEL_ID, TEST_PERMISSION_NAME, { + broker: MockWebChannelBroker + }); + let delivered = 0; + do_check_eq(channel.id, VALID_WEB_CHANNEL_ID); + do_check_eq(channel._originOrPermission, TEST_PERMISSION_NAME); + do_check_eq(channel._deliverCallback, null); + + channel.listen(function(id, message, target) { + do_check_eq(id, VALID_WEB_CHANNEL_ID); + do_check_true(message); + do_check_true(message.command); + do_check_true(target.sender); + delivered++; + // 2 messages should be delivered + if (delivered === 2) { + channel.stopListening(); + do_check_eq(channel._deliverCallback, null); + resolve(); + } + }); + + // send two messages + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "one" + } + }, { sender: true }); + + channel.deliver({ + id: VALID_WEB_CHANNEL_ID, + message: { + command: "two" + } + }, { sender: true }); + }); +}); + + +/** + * Test constructor + */ +add_test(function test_web_channel_constructor() { + do_check_eq(constructorTester(), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(undefined), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(undefined, VALID_WEB_CHANNEL_ORIGIN), ERROR_ID_ORIGIN_REQUIRED); + do_check_eq(constructorTester(VALID_WEB_CHANNEL_ID, undefined), ERROR_ID_ORIGIN_REQUIRED); + do_check_false(constructorTester(VALID_WEB_CHANNEL_ID, VALID_WEB_CHANNEL_ORIGIN)); + + run_next_test(); +}); + +function constructorTester(id, origin) { + try { + new WebChannel(id, origin); + } catch (e) { + return e.message; + } + return false; +} diff --git a/toolkit/modules/tests/xpcshell/test_web_channel_broker.js b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js new file mode 100644 index 000000000..132597c20 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_web_channel_broker.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/WebChannel.jsm"); + +const VALID_WEB_CHANNEL_ID = "id"; +const URL_STRING = "http://example.com"; +const VALID_WEB_CHANNEL_ORIGIN = Services.io.newURI(URL_STRING, null, null); + +function run_test() { + run_next_test(); +} + +/** + * Test WebChannelBroker channel map + */ +add_test(function test_web_channel_broker_channel_map() { + let channel = {}; + let channel2 = {}; + + do_check_eq(WebChannelBroker._channelMap.size, 0); + do_check_false(WebChannelBroker._messageListenerAttached); + + // make sure _channelMap works correctly + WebChannelBroker.registerChannel(channel); + do_check_eq(WebChannelBroker._channelMap.size, 1); + do_check_true(WebChannelBroker._messageListenerAttached); + + WebChannelBroker.registerChannel(channel2); + do_check_eq(WebChannelBroker._channelMap.size, 2); + + WebChannelBroker.unregisterChannel(channel); + do_check_eq(WebChannelBroker._channelMap.size, 1); + + // make sure the correct channel is unregistered + do_check_false(WebChannelBroker._channelMap.has(channel)); + do_check_true(WebChannelBroker._channelMap.has(channel2)); + + WebChannelBroker.unregisterChannel(channel2); + do_check_eq(WebChannelBroker._channelMap.size, 0); + + run_next_test(); +}); + + +/** + * Test WebChannelBroker _listener test + */ +add_task(function test_web_channel_broker_listener() { + return new Promise((resolve, reject) => { + var channel = { + id: VALID_WEB_CHANNEL_ID, + _originCheckCallback: requestPrincipal => { + return VALID_WEB_CHANNEL_ORIGIN.prePath === requestPrincipal.origin; + }, + deliver: function(data, sender) { + do_check_eq(data.id, VALID_WEB_CHANNEL_ID); + do_check_eq(data.message.command, "hello"); + do_check_neq(sender, undefined); + WebChannelBroker.unregisterChannel(channel); + resolve(); + } + }; + + WebChannelBroker.registerChannel(channel); + + var mockEvent = { + data: { + id: VALID_WEB_CHANNEL_ID, + message: { + command: "hello" + } + }, + principal: { + origin: URL_STRING + }, + objects: { + }, + }; + + WebChannelBroker._listener(mockEvent); + }); +}); diff --git a/toolkit/modules/tests/xpcshell/xpcshell.ini b/toolkit/modules/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000..65d7c45e9 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/xpcshell.ini @@ -0,0 +1,75 @@ +[DEFAULT] +head = +tail = +support-files = + propertyLists/bug710259_propertyListBinary.plist + propertyLists/bug710259_propertyListXML.plist + chromeappsstore.sqlite + zips/zen.zip + +[test_BinarySearch.js] +skip-if = toolkit == 'android' +[test_CanonicalJSON.js] +[test_client_id.js] +skip-if = toolkit == 'android' +[test_Color.js] +[test_DeferredTask.js] +skip-if = toolkit == 'android' +[test_FileUtils.js] +skip-if = toolkit == 'android' +[test_FinderIterator.js] +[test_GMPInstallManager.js] +skip-if = toolkit == 'android' +[test_Http.js] +skip-if = toolkit == 'android' +[test_Integration.js] +[test_jsesc.js] +skip-if = toolkit == 'android' +[test_JSONFile.js] +skip-if = toolkit == 'android' +[test_Log.js] +skip-if = toolkit == 'android' +[test_MatchPattern.js] +skip-if = toolkit == 'android' +[test_MatchGlobs.js] +skip-if = toolkit == 'android' +[test_MatchURLFilters.js] +skip-if = toolkit == 'android' +[test_NewTabUtils.js] +skip-if = toolkit == 'android' +[test_ObjectUtils.js] +skip-if = toolkit == 'android' +[test_ObjectUtils_strict.js] +skip-if = toolkit == 'android' +[test_PermissionsUtils.js] +skip-if = toolkit == 'android' +[test_Preferences.js] +skip-if = toolkit == 'android' +[test_Promise.js] +skip-if = toolkit == 'android' +[test_PromiseUtils.js] +skip-if = toolkit == 'android' +[test_propertyListsUtils.js] +skip-if = toolkit == 'android' +[test_readCertPrefs.js] +skip-if = toolkit == 'android' +[test_Services.js] +skip-if = toolkit == 'android' +[test_session_recorder.js] +skip-if = toolkit == 'android' +[test_sqlite.js] +skip-if = toolkit == 'android' +[test_sqlite_shutdown.js] +skip-if = toolkit == 'android' +[test_task.js] +skip-if = toolkit == 'android' +[test_timer.js] +skip-if = toolkit == 'android' +[test_UpdateUtils_url.js] +[test_UpdateUtils_updatechannel.js] +[test_web_channel.js] +[test_web_channel_broker.js] +[test_ZipUtils.js] +skip-if = toolkit == 'android' +[test_Log_stackTrace.js] +[test_servicerequest_xhr.js] diff --git a/toolkit/modules/tests/xpcshell/zips/zen.zip b/toolkit/modules/tests/xpcshell/zips/zen.zip new file mode 100644 index 000000000..475624793 Binary files /dev/null and b/toolkit/modules/tests/xpcshell/zips/zen.zip differ diff --git a/toolkit/modules/third_party/jsesc/README b/toolkit/modules/third_party/jsesc/README new file mode 100644 index 000000000..6665923c4 --- /dev/null +++ b/toolkit/modules/third_party/jsesc/README @@ -0,0 +1,10 @@ +This code comes from an externally managed library, available at +. Bugs should be reported directly +upstream and integrated back here. + +In order to regenerate this file, you need to do the following: + + $ git clone git@github.com:mathiasbynens/jsesc.git && cd jsesc + $ grunt template + $ export MOZ_JSESC="../mozilla-central/toolkit/modules/third_party/jsesc" + $ cat $MOZ_JSESC/fx-header jsesc.js > $MOZ_JSESC/jsesc.js diff --git a/toolkit/modules/third_party/jsesc/fx-header b/toolkit/modules/third_party/jsesc/fx-header new file mode 100644 index 000000000..fbac20f1c --- /dev/null +++ b/toolkit/modules/third_party/jsesc/fx-header @@ -0,0 +1,26 @@ +/* +DO NOT TOUCH THIS FILE DIRECTLY. See the README for instructions. + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +this.EXPORTED_SYMBOLS = ["jsesc"]; diff --git a/toolkit/modules/third_party/jsesc/jsesc.js b/toolkit/modules/third_party/jsesc/jsesc.js new file mode 100644 index 000000000..0145101d5 --- /dev/null +++ b/toolkit/modules/third_party/jsesc/jsesc.js @@ -0,0 +1,299 @@ +/* +DO NOT TOUCH THIS FILE DIRECTLY. See the README for instructions. + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +this.EXPORTED_SYMBOLS = ["jsesc"]; +/*! https://mths.be/jsesc v1.0.0 by @mathias */ +;(function(root) { + + // Detect free variables `exports` + var freeExports = typeof exports == 'object' && exports; + + // Detect free variable `module` + var freeModule = typeof module == 'object' && module && + module.exports == freeExports && module; + + // Detect free variable `global`, from Node.js or Browserified code, + // and use it as `root` + var freeGlobal = typeof global == 'object' && global; + if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { + root = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + var object = {}; + var hasOwnProperty = object.hasOwnProperty; + var forOwn = function(object, callback) { + var key; + for (key in object) { + if (hasOwnProperty.call(object, key)) { + callback(key, object[key]); + } + } + }; + + var extend = function(destination, source) { + if (!source) { + return destination; + } + forOwn(source, function(key, value) { + destination[key] = value; + }); + return destination; + }; + + var forEach = function(array, callback) { + var length = array.length; + var index = -1; + while (++index < length) { + callback(array[index]); + } + }; + + var toString = object.toString; + var isArray = function(value) { + return toString.call(value) == '[object Array]'; + }; + var isObject = function(value) { + // This is a very simple check, but it’s good enough for what we need. + return toString.call(value) == '[object Object]'; + }; + var isString = function(value) { + return typeof value == 'string' || + toString.call(value) == '[object String]'; + }; + var isFunction = function(value) { + // In a perfect world, the `typeof` check would be sufficient. However, + // in Chrome 1–12, `typeof /x/ == 'object'`, and in IE 6–8 + // `typeof alert == 'object'` and similar for other host objects. + return typeof value == 'function' || + toString.call(value) == '[object Function]'; + }; + + /*--------------------------------------------------------------------------*/ + + // https://mathiasbynens.be/notes/javascript-escapes#single + var singleEscapes = { + '"': '\\"', + '\'': '\\\'', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t' + // `\v` is omitted intentionally, because in IE < 9, '\v' == 'v'. + // '\v': '\\x0B' + }; + var regexSingleEscape = /["'\\\b\f\n\r\t]/; + + var regexDigit = /[0-9]/; + var regexWhitelist = /[ !#-&\(-\[\]-~]/; + + var jsesc = function(argument, options) { + // Handle options + var defaults = { + 'escapeEverything': false, + 'quotes': 'single', + 'wrap': false, + 'es6': false, + 'json': false, + 'compact': true, + 'lowercaseHex': false, + 'indent': '\t', + '__indent__': '' + }; + var json = options && options.json; + if (json) { + defaults.quotes = 'double'; + defaults.wrap = true; + } + options = extend(defaults, options); + if (options.quotes != 'single' && options.quotes != 'double') { + options.quotes = 'single'; + } + var quote = options.quotes == 'double' ? '"' : '\''; + var compact = options.compact; + var indent = options.indent; + var oldIndent; + var newLine = compact ? '' : '\n'; + var result; + var isEmpty = true; + + if (json && argument && isFunction(argument.toJSON)) { + argument = argument.toJSON(); + } + + if (!isString(argument)) { + if (isArray(argument)) { + result = []; + options.wrap = true; + oldIndent = options.__indent__; + indent += oldIndent; + options.__indent__ = indent; + forEach(argument, function(value) { + isEmpty = false; + result.push( + (compact ? '' : indent) + + jsesc(value, options) + ); + }); + if (isEmpty) { + return '[]'; + } + return '[' + newLine + result.join(',' + newLine) + newLine + + (compact ? '' : oldIndent) + ']'; + } else if (!isObject(argument)) { + if (json) { + // For some values (e.g. `undefined`, `function` objects), + // `JSON.stringify(value)` returns `undefined` (which isn’t valid + // JSON) instead of `'null'`. + return JSON.stringify(argument) || 'null'; + } + return String(argument); + } else { // it’s an object + result = []; + options.wrap = true; + oldIndent = options.__indent__; + indent += oldIndent; + options.__indent__ = indent; + forOwn(argument, function(key, value) { + isEmpty = false; + result.push( + (compact ? '' : indent) + + jsesc(key, options) + ':' + + (compact ? '' : ' ') + + jsesc(value, options) + ); + }); + if (isEmpty) { + return '{}'; + } + return '{' + newLine + result.join(',' + newLine) + newLine + + (compact ? '' : oldIndent) + '}'; + } + } + + var string = argument; + // Loop over each code unit in the string and escape it + var index = -1; + var length = string.length; + var first; + var second; + var codePoint; + result = ''; + while (++index < length) { + var character = string.charAt(index); + if (options.es6) { + first = string.charCodeAt(index); + if ( // check if it’s the start of a surrogate pair + first >= 0xD800 && first <= 0xDBFF && // high surrogate + length > index + 1 // there is a next code unit + ) { + second = string.charCodeAt(index + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { // low surrogate + // https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + var hexadecimal = codePoint.toString(16); + if (!options.lowercaseHex) { + hexadecimal = hexadecimal.toUpperCase(); + } + result += '\\u{' + hexadecimal + '}'; + index++; + continue; + } + } + } + if (!options.escapeEverything) { + if (regexWhitelist.test(character)) { + // It’s a printable ASCII character that is not `"`, `'` or `\`, + // so don’t escape it. + result += character; + continue; + } + if (character == '"') { + result += quote == character ? '\\"' : character; + continue; + } + if (character == '\'') { + result += quote == character ? '\\\'' : character; + continue; + } + } + if ( + character == '\0' && + !json && + !regexDigit.test(string.charAt(index + 1)) + ) { + result += '\\0'; + continue; + } + if (regexSingleEscape.test(character)) { + // no need for a `hasOwnProperty` check here + result += singleEscapes[character]; + continue; + } + var charCode = character.charCodeAt(0); + var hexadecimal = charCode.toString(16); + if (!options.lowercaseHex) { + hexadecimal = hexadecimal.toUpperCase(); + } + var longhand = hexadecimal.length > 2 || json; + var escaped = '\\' + (longhand ? 'u' : 'x') + + ('0000' + hexadecimal).slice(longhand ? -4 : -2); + result += escaped; + continue; + } + if (options.wrap) { + result = quote + result + quote; + } + return result; + }; + + jsesc.version = '1.0.0'; + + /*--------------------------------------------------------------------------*/ + + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define(function() { + return jsesc; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = jsesc; + } else { // in Narwhal or RingoJS v0.7.0- + freeExports.jsesc = jsesc; + } + } else { // in Rhino or a web browser + root.jsesc = jsesc; + } + +}(this)); -- cgit v1.2.3