diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 00000000..86d2cef6 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,37 @@ +genrule( + name = "get-attribute", + srcs = ["//javascript/webdriver/atoms:get-attribute.js"], + outs = ["selenium/webdriver/remote/getAttribute.js"], + cmd = "cp $< $@", +) + +genrule( + name = "is-displayed", + srcs = ["//javascript/atoms/fragments:is-displayed.js"], + outs = ["selenium/webdriver/remote/isDisplayed.js"], + cmd = "cp $< $@", +) + +py_library( + name = "main", + srcs = glob(["selenium/**/*.py"]), + data = [ + ":get-attribute", + ":is-displayed", + ], + imports = ["."], + visibility = ["//visibility:public"], +) + +py_test( + name = "unit", + size = "small", + srcs = glob([ + "test/unit/**/*.py", + ]) + [ "test/run_pytest.py" ], + main = "test/run_pytest.py", + deps = [ + ":main", + ], + legacy_create_init = False, +) \ No newline at end of file diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..1ea15ac8 --- /dev/null +++ b/CHANGES @@ -0,0 +1,701 @@ +Selenium 4.0 Alpha 1 +* Update driver initialisation to use service and option objects +* turn on keep-alive by default for remote connections (#7072) +* Fix ConnectionResetError +* Add new Cast commands +* Suggest download Microsoft Webdriver over HTTPS +* Clear PoolManager in ‘remote_connection’ to ensure sockets are closed +* remove --disable-gpu option for headless Chrome +* Add support for the New Window command (#6873) +* Update docstrings in Options classes to allow documentation to highlight Return values +* Fix typos in select.py (#6925) +* Remove native events handling code +* Deleting unused imports, fixing flake8 issues +* Remove unused port selection in IE Driver +* Enabling tests xpassed in Chrome +* Pretty-printing code samples +* remove all deprecated methods and args from Python bindings +* Fix DeprecationWarning: invalid escape sequence +* Don't override browser options with desired capabilities by default in WebKitGTK (#6814) +* Add WebKitGTK to API docs (#6815) +* Subclass options classes from a common base class (#6522) +* Update Sphinx (#6728) +* WebDriverWait: update documentation for until and until_not (#6711) +* Fix typo in description of WebDriver class (#6724) +* add strictFileInteractability to acceptable W3C capabilities +* Deprecate Blackberry Driver support +* Fixing/tidying docstring. + + +Selenium 3.141.0 +* Bump version to a better approximation of Π +* Improved Test build targets +* fix os path in test for Windows +* use 'NUL' for /dev/null on Windows +* Update ctor docstrings to explain that a directory passed in is cloned. Fixes #6542 +* Allow passing of service_args to Safari. Fixes #6459 +* Remove element equals url +* Improved WebExtension support + +Selenium 3.14.1 +* Fix ability to set timeout for urllib3 (#6286) +* get_cookie uses w3c endpoint when compliant +* Remove body from GET requests (#6250) +* Fix actions pause for fraction of a second (#6300) +* Fixed input pausing for some actions methods +* Capabilities can be set on Options classes +* WebElement rect method is now forward compatible for OSS endpoints (#6355) +* Deprecation warnings now have a stacklevel of 2 +* keep_alive can now be set on Webdriver init (#6316) +* isDisplayed atom is now used for all w3c compliant browser, fixing issue with Safari 12 + +Selenium 3.14.0 +* Fix doc of URL-related ExpectedCondition (#6236) +* Added ExpectedCondition invisibility_of_element +* Swap out httplib for urllib3 +* Be consistent with webdriver init kwarg service_log_path (#5725) + +Selenium 3.13.0 + +* Add executing Chrome devtools command (#5989) +* fix incorrect w3c action encoding in python client (#6014) +* Implement context manager for WebDriver +* Stop sending "windowHandle" param in maximize_window command for w3c + +Selenium 3.12.0 + +* Add desired_capabilities keyword to IE and Firefox drivers for driver consitency +* Fix bug with creating Safari webdriver instance (#5578) +* Add support for Safari extension command +* Deprecate Options `set_headless` methods in favor of property setter +* Only set --disable-gpu for Chrome headless when on Windows +* Add selenium User-Agent header (#5696) +* Remote webdriver can now be started when passing options +* All Options.to_capabilities now start with default DesiredCapabilities +* Improve the error message that is raised when safaridriver cannot be found (#5739) +* IeOptions class is now imported to selenium.webdriver +* Remove the beta `authenticate` methods from `Alert` + +Selenium 3.11.0 + +No changes just keeping python version in step with the rest of the project. + +Selenium 3.10.0 + +* make tests to check clicking on disabled element work for w3c compliant drivers (#5561) +* add docstring for InvalidElementStateException. Fixes #5520 +* Deleting unused imports +* Making python specification in IDEA project more generic +* It should be possible to use a custom safaridriver executable to run Selenium's test suite. + +Selenium 3.9.0 + +* Add docstrings to WebElement find methods (#5384) +* Additional data in unexpected alert error is now handled for w3c drivers (#5416) +* Allow service_args to be passed into Firefox WebDriver (#5421) +* Fix bug introduced with response logging in 3.8.1 (#5362) + +Selenium 3.8.1 + +* Fix bug when creating an Opera driver (#5266) +* Stop sending sessionId in w3c payload. (#4620) +* Fix issue with w3c actions releasing on element (#5180) +* A more descriptive log message is displayed if the port cannot be connected (#2913) +* Initialize Alert object by calling alert.text (#1863) +* PhantomJS is now deprecated, please use either Chrome or Firefox in headless mode +* Legacy Firefox driver: ensuring copy of profile dir, its 'extensions' subdir and 'user.js' file are writable. (#1466) + +Selenium 3.8.0 + +* Firefox options can now be imported from selenium.webdriver as FirefoxOptions (#5120) +* Headless mode can now be set in Chrome Options using `set_headless` +* Headless mode can now be set in Firefox Options using `set_headless` +* Add the WebKitGTK WebDriver and options class (#4635) +* Browser options can now be passed to remote WebDriver via the `options` parameter +* Browser option parameters are now standardized across drivers as `options`. `firefox_options`, + `chrome_options`, and `ie_options` are now deprecated +* Added missing W3C Error Codes (#4556) +* Support has been removed for Python versions 2.6 and 3.3 + +Selenium 3.7.0 + +* need to pass applicable environment variables to tox +* Fix active_element for w3c drivers (#3979) +* add support for minimize command +* add support for fullscreen command +* window rect commands should fail on firefox and remote (legacy) +* Fix python backward compatibility for window commands (#4937) +* Update docstrings to specify the type of path needed to install firefox addons. (#4881) +* Update python chromeOptions key for capabilities (#4622) +* Fix python pause action implementation (#4795) + +Selenium 3.6.0 + +* Fix package name in python webelement module (#4670) +* Fix python driver examples (#3872) +* No need to multiply pause by 1000 +* Add pause to action chains +* only check for proxyType once +* lowercase proxy type for w3c payload in python #4574 +* guarding against null return value from find_elements in python #4555 +* remove unnecessary pytest marking, address flake8 issues +* allow IE WebDriver to accept IE Options +* add IE Options class +* convert OSS capabilities to W3C equivalent for W3C payload +* Add Safari to API docs + +Selenium 3.5.0 + +* Numerous test fixes +*Iterate over capabilities in a way to support py2.7 and py3 +* Fix W3C switching to window by name. +* Support GeckoDriver addon install/uninstall commands #4215. +* Move firefox_profile into moz:firefoxOptions. +* Filter non-W3C capability names out of alwaysMatch. +* Honor cmd line args passed to Service ctor (#4167) +* Add expected conditions based on URL to Python Expected Conditions #4160 +* Add network emulation to Chrome Python bindings (#4011) +* add warning when saving incorrectly named screenshot (#4141) + +Selenium 3.4.3 +* Fix EventFiringWebdriver and WebElement to raise AttributeError on missing attributes. (#4107) +* unwrap WebElements inside dicts + +Selenium 3.4.2 + +* translate move_by_offset command to w3c +* Update capabilities properly instead of assuming dict structure. Fixes #3927 +* Add missing file for Chrome options to API docs. +* Add Chrome options module to API docs. + +Selenium 3.4.1 +* Add back the ability to set profile when using Firefox 45ESR. Fixes #3897 + +Selenium 3.4.0 +* Correct usage of newSession around `firstMatch` and `alwaysMatch` +* Remove superfluous capabilities that are not needed +* Add expected condition that waits for all found elements to be visible (#3532) +* Allow methods wrapped by EventFiringWebDriver and EventFiringWebElement (#806) +* Dropping `javascriptEnabled` capability for real browsers +* Use W3C check from parent object instead of assuming from capabilities +* Bump example source distribution to match latest release. +* Replace TypeError with KeyError in remote webdriver error handler code (#3826) +* When testing Marionette use default capabilities in testing +* Conform to the api of urllib2 for adding header for a request (#3803) +* Add `text` key to alert#sendKeys parameters for W3C Endpoint +* Location once scrolled into view should use W3C executeScript endpoint not JSONWP +* Fixed the usage information in documentation of "save_screenshot". (#3804) +* Add Element Not Interactable exception +* Clean up imports in error handler +* flake8 cleanup + +Selenium 3.3.3 + +* make w3c execute_script commands unique + +Selenium 3.3.2 + +* Update window commands to use W3C End points +* Update Alert when in W3C mode to use W3C Endpoints +* Update to new W3C Execute Script end points +* Add setting/getting proxy details to Firefox Options +* Deprecate the use of browser profile when instantiating a session +* Update start session to handle the W3C New Session +* Add get/set window rect commands +* Add InvalidArgumentException +* When passing in `text` to send_keys, make sure we send a string not array +* Fix string decoding in remote connection (#3663) +* Fix indentation to satisfy PEP8 +* Try use old way of setting page load timeout if new way fails. Fixes #3654 +* fix file uploads for Firefox +* Run unit tests on Python 3.3, 3.4, and 3.5 (#3638) +* Fix indentation in double_click. +* Fix non-W3C page load timeout setting. + +Selenium 3.3.1 +* Fix encoding of basic auth header when using Python 3 Fixes #3622 +* Add initial unit test suite +* Update W3C Timeout setting to be in line with the specification +* support.ui.Select class inherits from object (#3067) +* fix bug in proxy constructor that some properties are not proper set (#3459) +* Fix flake8 issues (#3628) + +Selenium 3.3.0 +** Note ** If you are updating to this version, please also update GeckoDriver to v0.15.0 +* Fix python HTTPS encoding for python driver (#3379) +* Allow Firefox preferences to be set directly in Options +* Fix shutdown and process termination (#3263) +* Preventing exception if log_path is none or empty. Fixes #3128 +* Add the W3C capability to Firefox for accepting insecure certificates +* Initial implementation of Pointer Actions +* Only skip tests if driver name matches a directory name. +* Update calls that return a pure object with keys to look for 'value' key +* Initial W3C Actions support +* fix docs output directory + +Selenium 3.0.2 +* Add support for W3C Get Active Element +* Return when we use executeScript for faking WebElement.get_property +* Expand user paths and resolve absolute path for Chrome extensions +* Add support for verbose logging and custom path to EdgeDriver +* Update TakeElementScreenshot to match WebDriver specification +* Raise WebDriverException when FirefoxBinary fails to locate binary +* Fix getting IP for python 3 +* Write Service log to DEVNULL by default +* Only attempt to remove the Firefox profile path if one was specified +* Added context manager for chrome/content in Firefox + +Selenium 3.0.1 +* Fix regressions with python 3 +* Add support for Safari Technology Preview + +Selenium 3.0.0 +* new FirefoxDriver ctor precedence logic and moz:firefoxOptions support (#2882) +* Add W3C Set Window Position and W3C Get Window Position +* enable log path setting from firefox webdriver (#2700) +* Correct encoding of getAttribute.js and only load it once. Fixes #2785 +* Encode the isDisplayed atom and only load it once + +Selenium 3.0.0.b3 +* Use atoms for get_attribute and is_displayed when communicating with + a w3c compliant remote end. +* Make it possible to specialise web element + +Selenium 3.0.0.b2 +* Updated Marionette port argument to match other drivers. + +Selenium 3.0.0.b1 +* Fix basestring reference to work with python 3. Fixes #1820 +* Correct Length conditional when filtering in PhantomJS. Fixes #1817 +* Fix send keys when using PUA keys e.g. Keys.RIGHT #1839 +* Fix cookie file leak in PhantomJS #1854 +* Use the correct binary path when using Marionette +* Fixed: Unhelpful error message when PhantomJS exits. (#2173 #2168) +* Fix broken link to python documentation (#2159) +* Attempt to remove Firefox profile when using Marionette +* Ensure all capabilities are either within desiredCapabilities or requiredCapabilities +* Correct the expected capability name for the Firefox profile +* Add Firefox options to capabilities +* Visibility_of_all implies it only returns elements if all visible (#2052) +* Find visible elements (#2041) +* Pass the firefox_profile as a desired capability in the Python client when using a remote server +* Avoid checking exception details for invalid locators due to differences in server implementations +* Handle capabilities better with Marionette and GeckoDriver +* Updated the maxVersion of FirefoxDriver xpi maxVersion to work with Firefox 47.0.1 +* Remove Selenium RC support + +Selenium 2.53.0 +* Adding Options object for use with Python FirefoxDriver +* Fixed improper usage of super in exceptions module +* create a temp file for cookies in phantomjs if not specified +* Pass in the executable that FirefoxBinary finds to the service if there isnt one passed in as a kwarg or capability +* Applied some DRY and extracted out the keys_to_typing() +* Fix deselecting options in <select> + + +Selenium 2.52.0 +* Fixing case where UnexpectedAlertException doesn't get the alert_text in the error object +* Firefox: Actually use launch_browser timeout Fixes #1300 + +Selenium 2.51.1 +* correcting bundling issue missing README.rst file + +Selenium 2.51.0 +* Firefox updates (see java changelog) + +Selenium 2.50.1 +* Fixing error message handling. Fixes issue #1497 +* Fixing error message handling. Fixes issue #1507 +* Update webelement to handle W3C commands for size/location and rect +* rewrite click scrolling tests to match the Java ones + +Selenium 2.50.0 +* handle potential URLError from sending shutdown, set self.process to None after it's already been quit +* Add support for submit() with W3C compliant endpoint + +Selenium 2.49.1 +* Ensure you can close stream before attempting to close it. +* message response may cause json loads ValueError when it's not actually json + and just a string (like the message that occurs when firefox driver thinks + another element will receive the click) +* Cleanup some error handling when sniffing what protocol you are speaking + +Selenium 2.49.0 +* Have Firefox service write to a file instead of PIPE +* on osx for firefox, fallback to checking homebrew install, if the default isn't there +* Added Firefox path variable for string placeholder +* Update README to show Python 3.2+ +* refactoring all the service classes to use a common one. +* Add Firefox specific command to switch context between Browser content and Browser chrome +* updating files after go copyright:update +* Use specificationLevel to know that we are speaking GeckoDriver +* Bug fixes: #1294, #1186 + +Selenium 2.48.0 +* Update error pulling to match spec when we encounter a spec compliant browser. +* Disable tests that are not working with Marionette when running Marionette tests +* Add the ability to run python marionette tests +* Python 3 compatibility for remote Authorization +* changing casing of children finding tests + +Selenium 2.47.3 +* Bring back py 3 support + +Selenium 2.47.2 +* Fix running Edge driver locally on win10 +* adding repr to WebDriver and WebElement + +Selenium 2.47.1 +* Fix the issue of deleting the profile when shutting down Firefox +* WebElement __eq__ compares against more types +* Issues fixed: 850 + +Selenium 2.47.0 +* Add in support for when communicating with a Spec compliant browsers +* Initial support for Edge using EdgeDriver +* Issues fixed: 818 + +Selenium 2.46.1 +* Adding ability to make remote call for webelement screenshots in accordance to the W3C spec +* Adding api to authenticate HTTP Auth modal dialogs via driver.switch_to.alert (beta) +* Add rebeccapurple to Color Object +* Add element screenshot +* Add service handler and minimal update to driver to use service for Marionette +* Add the ability to start FirefoxDriver backed with Marionette via a capability +* support socket timeout for connections +* free_port checks if port is available on all interfaces +* Allow error handling to handle both current errors and w3c errors +* Update find_elements to match spec +* phantomjs: service: remove unused import of signal +* phantomjs: add port information to WebDriverException +* Issues fixed (Github): 478, 612, 734, 780 + +Selenium 2.46.0 +* Firefox support up to 38 +* BlackBerry browser support +* remove Presto-Opera support +* firefox extension extraction fixes +* process management fixes with phantomjs +* Comparing remote web element for equality does not require a remote command +* Issues Fixed: (gcode) 8493, 8521, 8498, 8274, 8497, 5923 +* Issues Fixed: (github) 401 + +Selenium 2.45.0 +* Firefox support up to 35, support for native events up to 34. +* Make Opera driver support also the new Blink based Opera +* README: Fix the Yahoo example +* WebElement docstring fixes +* Add debugger_address option to the ChromeDriver options list to optionally instruct ChromeDriver to wait for the target devtools instance to be started at a given host:ip +* Set default value for PhantomJS process reference +* Allow setting of FileDetector for send_keys +* Pass info to TimeoutException in WebDriverWait +* Issues Fixed: 8065, 8310, 8539 + +Selenium 2.44.0 +* (previous release person forgot to add release notes! DAVID!) + +Selenium 2.43.0 +* Expand WebElement.get_attribute API docs +* firefox may be installed without admininstrator privileges + and therefore there may be no HKEY_LOCAL_MACHINE entry. Issue #7784 +* UnexpectedAlertPresentException should contain the alert text in python too. Issue #7745 +* don't mutate the global 'ignored exceptions', take a copy of the globally specified ones, change the + global to be a tuple instead. Issue #7725 +* raise exception when the firefox binary isn't actually found, which usually implies the upgrade failed (on windows) Issue #6092 ,#6847 +* Fixing NameError: global name 'options' is not defined. +* Remove unused import subprocess.PIPE +* Redirect Firefox output to /dev/null by default Fixes Issue #7677 +* More flexible management of http timeouts in Selenium RC python client +* Generate Python API docs for selenium.webdriver.chrome.options. Fixes issue #7471 +* Use 127.0.0.1 as localhost name resolving might fail on some systems + +Selenium 2.42.1 +* Fixed Py3 issues +* Make firefox_binary.py and firefox_profile.py not executable +* Make exceptions Python 3 compatible + +Selenium 2.42 +* Support for Firefox 29 Native Events +* "remote_url" and "remote_browser" parameters for "./go test_remote". +* missing __init__ in new android module +* issue #7304 Fix memory leak caused by __del__ in PhantomJS +* File upload using remotedriver on python3 +* Updating xpi install to align with mozprofile +* command_executor should also support unicode strings as well. + +Selenium 2.41 +* Support for Firefox 28 +* deprecating switch_to_* in favour of driver.switch_to.* + +Selenium 2.40 +* Support for Firefox 27 +* Fixes related to http connection +* Fix for phantomjs running on windows #6736 + +Selenium 2.39 +* Support for Firefox 26 + +Selenium 2.38.4 +* keep-alive can't be used for phantomjs / IE, fix for that and tested for py3 :) + +Selenium 2.38.3 +* really supporting py3 :) + +Selenium 2.38.2 +* py3 support (once again) + +Selenium 2.38.1 +* fix packaging problem where firefox/webdriver_prefs.json was missing + +Selenium 2.38 +* Support for Firefox 25 +* FirefoxProfile now using common webdriver.json instead of having our own copy in py + - behavior change to the preferences is that they now should be treated + like raw types rather than strings and allow the json library to translate + the types appropriated (e.g. True => true) + +* Set proper 'Accept' request header so that Python bindings work with some old WebDriver implementations that reply 404 to requests with no 'Accept' set. +* handle redirect response explicitly (since switching to using keep-alive) +* phantomjs service needs to really kill the spawned process Issue #5921 +* removing old api endpoints from command listing +* using keep-alive for remote connection +* adjusting phantomjs subprocess.Popen +* ActionsChains.send_keys should use <session>/keys endpoint Issue #6348 +* fix TypeError in chrome_options.extensions for Python3.x + +* Other Bugs Fixed: #6531, #6513, #4569, #6454 + + +Selenium 2.37.2 +* fix regression added with unicode fix +* Bug fix #6360 + +Selenium 2.37.1 +* fix find_elements on webelement using unicode locators and py 2.7 + +Selenium 2.37 +* repackage with fix for Firefox native events on Linux +* fix issue with unicode By locators w/ python 2.7 #6430 + +Selenium 2.36 +* Added Safari WebDriver. Fixes issue 5352. +* fix platform for safari caps +* Convert all offsets/coordinates/speeds into integers +* Fix drag and drop by offset behaviour +* Fix initialization of Proxy by capabilities when proxyType is set +* Enable SOCKS proxy support +* Validation of passed locator for find_element(s) methods #5690 +* Adding support for /status, /sessions commands +* Doc fixes +* ability to set Chrome extensions by using base64 encoded strings #4013 +* fix logic regarding Select.select_by_visible_text #3910 +* Bugs fixed: #6165, #6231 + +Selenium 2.35 +* Remove duplicate 'get screenshot as file' methods. Add method 'get_screenshot_as_png' +* fixing UnicodeEncodeError on get attribute of webelement + +Selenium 2.34 +* Corrected webdriverbackedselenium session handling. Fixes issue 4283 +* Corrected use of basestring for python 3. Fixes issue 5924 +* Support for Firefox 22 +* Added support for logging from the browser +* corrected proxy handling on FirefoxProfile +* Corrected handling of chrome extensions. Fixes issue 5762 + +Selenium 2.33 +* getText() ignores elements in the <head> +* Adding both official and informal string representations to Color object. +* Replace distutils.dir_util by shutil +* Allow finding firefox binary at ProgramFiles(x86) on windows(64 bit) +* Py3 compatible winreg import and content-type access + +Selenium 2.32 +* Support for FF20 Native Events +* Python 3 support +* Misc Python 3 patches +* Allow easy FirefoxBinary subclassing + +Selenium 2.31 +* Support for FF19 native events +* web element equality is now in conformance with other language bindings + +Selenium 2.30 +* Allow env to be specified for the chromedriver service +* Allow log path to be specified for phantomjs driver service. +* Bug Fixes: 4608 4940 4974 5034 5075 + +Selenium 2.29 +* Allow subclassing of driver and have the ability to send_keys Issue 4877, 5017 +* Simplifying save_screenshot and allow phantomjs to take screenshots + +Selenium 2.28 +* "null" can now be passed to executeScript +* Add transparent and extended colour keywords to color support module. Fixes issue 4866 + +Selenium 2.27 +* Added support for phantomjs / ghostdriver +* Fix python client, avoid duplicate chrome option items after reusing options class. Fixes Issue 4744. +* adding colour support to Python. fixes issue 4623 +* Adding log_path/service_log_path as named kwargs for chrome + +Selenium 2.26 +* Added location_when_scrolled_into_view - Bug 4357 +* Added new expected_conditions support module to be used with WebDriverWait + +Selenium 2.25 +* Jython 2.7 Support - Bug 3988 +* EventFiringWebDriver added to Support module - Bug 2267 +* Added IEDriverServer logging that can be accessed via desired capabilities +* Fixed by data being passed into find_elements - bug 3735 +* Removed deprecated ChromeDriver items around desiredcapabilites in favour of chrome options +* Added default values for a number of action_chains calls + +Selenium 2.24 +* Removing the ctypes approach of invoking IEDriver, you will need to download the IEDriverServer from + https://code.google.com/p/selenium/downloads/list + +Selenium 2.23 +* Support for FF13 native events + +Selenium 2.22 +* Moving IEDriver to be able to use IEDriverServer + +Selenium 2.21.3 +* Fix for File Upload to remote servers +* Better handling of typing in input=file. Bug 3831, 3736 +* Better handling of unicode URLS Bug 3740 + +Selenium 2.21.2 +* Fix typing to file input when not using Selenium Server. Bug 3736 + +Selenium 2.21.1 +* focusmanager.testmode messes with native events, removing it. + +Selenium 2.21 +* Local File upload capabilities for non-remote browser +* Adding maximize_window api call +* Updating default firefox profile to set focusmanager.testmode to true + see https://bugzilla.mozilla.org/show_bug.cgi?id=704583 +* bugs fixed: 3506, 3528, 3607 + +Selenium 2.20 +* disable native events for FF on Mac by default +* fix webdriverwait to execute at least once when using 0 timeout +* Fixed Issue 3438 + +Selenium 2.19 +* WebDriverBackedSelenium is now avalaible to all languages +* Addon installation fixes + +Selenium 2.18 +* Proxy capabilities passing + +Selenium 2.17 +* OperaDriver can now be invoked by webdriver.Opera() +* Support has been added for ChomeOptions. This deprecates support passing in DesiredCapabilities +* Proxy class to tell the browser a proxy is in use. Currently only for Firefox + +Selenium 2.16 +* bug fixes + +Selenium 2.15 +* bug fixes + +Selenium 2.14 +* Fix for LD_PRELOAD being polluted by WebDriver +* Added Orientation API +* A fix for Error Handling + +Selenium 2.13 +* Fixed switch_to_window so that it didnt crash Firefox Bug 2633 +* Fixed Screenshot handling to work in all browsers. Bug 2829 +* Force Firefox to the Foreground + +Selenium 2.12 +* Added Select as a support pacakge +* Added Beta window size / position api's +* Bug Fixes + +Selenium 2.11.0 2.11.1 +* no changes just packaging + +Selenum 2.10 +* "Choose which apps" dialog has been disabled +* Bug Fixes + +Selenium 2.9 +* Bug Fixes +* Documentation + +Selenium 2.8 +* Actions updates +* Bug Fixes + +Selenium 2.6 +* Documentation fixes + +Selenium 2.5 +* Fixed x64 IE Support +* Bug Fixes + +Selenium 2.4 +* Bug Fixes +* x64 IE Support +* Added WebDriverWait as a support package + +Selenium 2.3 +* Bug Fixes + +Selenium 2.2 +* Ability to get screenshots from Exceptions if they are given +* Access to Remote StackTrace on error + +Selenium 2.1 +* Bug Fixes + +Selenium 2 +* Removed toggle() and select() + +Selenium 2 RC 3 +* Added Opera to Desired Capabilities +* Removed deprecrated methods +* Deprecated toggle() and select() methods. This will be removed in the next release + +Selenium 2 Beta 4 +* Fix for using existing Firefox Profiles +* Alerts Support in IE +* Fix to dictionary returned from size +* Deprecated value property. Use the get_attribute("value") method +* Deprecated get_page_source method. Use page_source property +* Deprecated get_current_window_handle. Use current_window_handle property +* Deprecated get_window_handles. Use window_handles property +* Ability to install extensions into profiles +* Added Location to the WebElement +* ChromeDriver rewritten to use new built in mechanism +* Added Advanced User Interaction API. Only Available for HTMLUnit at the moment +* Profiles now delete their temp folders when driver.quit() is called + +Selenium 2 Beta 3 +* Accept Untrusted Certificates in Firefox +* Fixed Screenshots +* Added DesiredCapabilities to simplify choosing Drivers +* Fixed Firefox Profile creation +* Added Firefox 4 support +* DocStrings Improvements + +Selenium 2 Beta 2 + +* New bindings landed. Change webdriver namespace to "selenium.webdriver" +* Ability to move to default content +* Implicit Waits +* Change the API to use properties instead of get_x +* Changed the Element Finding to match other languages +* Added ability to execute asynchronous scripts from the driver +* Ability to get rendered element size +* Ability to get CSS Value on a webelement +* Corrected Element finding from the element +* Alert and Prompt handling +* Improved IEDriver +* Basic Authentication support for Selenium 2 +* Ability to have multiple Firefox instances diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..a9114391 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,20 @@ +prune * +recursive-include selenium/webdriver *.py +recursive-include selenium/webdriver/common *.py +recursive-include selenium/webdriver/common/actions *.py +recursive-include selenium/webdriver/common/html5 *.py +recursive-include selenium/common *.py +recursive-include selenium/webdriver/chrome *.py +recursive-include selenium/webdriver/opera *.py +recursive-include selenium/webdriver/phantomjs *.py +recursive-include selenium/webdriver/firefox *.py *.xpi *.json +recursive-include selenium/webdriver/ie *.py +recursive-include selenium/webdriver/edge *.py +recursive-include selenium/webdriver/remote *.py *.js +recursive-include selenium/webdriver/support *.py +include selenium/selenium.py +include selenium/__init__.py +include CHANGES +include README.rst +include LICENSE +recursive-include selenium.egg-info * diff --git a/README.rst b/README.rst new file mode 120000 index 00000000..57b939dd --- /dev/null +++ b/README.rst @@ -0,0 +1 @@ +docs/source/index.rst \ No newline at end of file diff --git a/build.desc b/build.desc new file mode 100644 index 00000000..78e85636 --- /dev/null +++ b/build.desc @@ -0,0 +1,85 @@ +# py_test targets emplicitly get the following extra targets added: +# for each browser in browsers: +# :name_B to gather the sources of the tests for browser B +# :name_B:run to run the tests for browser B +# Also, if only one browser, B, is listed in browsers: +# :name:run is created as a synonym for :name_B:run +# +# Currently, pulling in other tests through deps +# is limited to deps declared in the same build.desc file + +py_test( + name = "firefox_test", + deps = [ ":test_ff" ], + resources = [ + { "//third_party/js/selenium:webdriver_prefs" : "selenium/webdriver/firefox/webdriver_prefs.json" }, + { "//third_party/js/selenium:webdriver" : "selenium/webdriver/firefox/" } + ], + browsers = [ "ff" ]) + +py_test( + name = "marionette_test", + deps = [ ":test_marionette" ], + browsers = [ "marionette" ]) + +py_test( + name = "blackberry_test", + deps = [ ":test_blackberry" ], + browsers = [ "blackberry" ]) + +py_test( + name = "chrome_test", + deps = [ ":test_chrome" ], + browsers = [ "chrome" ]) + +py_test( + name = "ie_test", + deps = [ ":test_ie" ], + browsers = [ "ie" ]) + +py_test( + name = "edge_test", + deps = [":test_edge"], + browsers = [ "edge"]) + +py_test( + name = "remote_firefox_test", + deps = [ ":test_remote_firefox" ], + browsers = [ "remote_firefox" ]) + +py_test( + name = "safari_test", + deps = [ ":test_safari" ], + browsers = [ "safari" ]) + +py_test( + name = "test", + browsers = [ + "chrome", + "ff", + "marionette", + "ie", + "edge", + "blackberry", + "remote_firefox", + "safari", + ]) + +py_docs( + name = "docs" +) + +py_install( + name = "install" +) + +py_prep( + name = "prep", + deps = [ + "//cpp:noblur", + "//cpp:noblur64", + "//javascript/atoms/fragments:is-displayed", + "//third_party/js/selenium:webdriver", + "//third_party/js/selenium:webdriver_prefs", + "//javascript/webdriver/atoms:get-attribute" + ]) diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..7ba634a0 --- /dev/null +++ b/conftest.py @@ -0,0 +1,219 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +import socket +import subprocess +import sys +import time + +import pytest +from _pytest.skipping import MarkEvaluator + +from selenium import webdriver +from selenium.webdriver import DesiredCapabilities +from test.selenium.webdriver.common.webserver import SimpleWebServer +from test.selenium.webdriver.common.network import get_lan_ip + +if sys.version_info[0] == 3: + from urllib.request import urlopen +else: + from urllib import urlopen + +drivers = ( + 'BlackBerry', + 'Chrome', + 'Edge', + 'Firefox', + 'Ie', + 'Marionette', + 'Remote', + 'Safari', + 'WebKitGTK', +) + + +def pytest_addoption(parser): + parser.addoption('--driver', action='append', choices=drivers, dest='drivers', + metavar='DRIVER', + help='driver to run tests against ({})'.format(', '.join(drivers))) + parser.addoption('--browser-binary', action='store', dest='binary', + help='location of the browser binary') + parser.addoption('--driver-binary', action='store', dest='executable', + help='location of the service executable binary') + parser.addoption('--browser-args', action='store', dest='args', + help='arguments to start the browser with') + + +def pytest_ignore_collect(path, config): + drivers_opt = config.getoption('drivers') + _drivers = set(drivers).difference(drivers_opt or drivers) + if drivers_opt: + _drivers.add('unit') + parts = path.dirname.split(os.path.sep) + return len([d for d in _drivers if d.lower() in parts]) > 0 + + +driver_instance = None + + +@pytest.fixture(scope='function') +def driver(request): + kwargs = {} + + try: + driver_class = request.param + except AttributeError: + raise Exception('This test requires a --driver to be specified.') + + # conditionally mark tests as expected to fail based on driver + request.node._evalxfail = request.node._evalxfail or MarkEvaluator( + request.node, 'xfail_{0}'.format(driver_class.lower())) + if request.node._evalxfail.istrue(): + def fin(): + global driver_instance + if driver_instance is not None: + driver_instance.quit() + driver_instance = None + request.addfinalizer(fin) + + # skip driver instantiation if xfail(run=False) + if not request.config.getoption('runxfail'): + if request.node._evalxfail.istrue(): + if request.node._evalxfail.get('run') is False: + yield + return + + driver_path = request.config.option.executable + options = None + + global driver_instance + if driver_instance is None: + if driver_class == 'BlackBerry': + kwargs.update({'device_password': 'password'}) + if driver_class == 'Firefox': + kwargs.update({'capabilities': {'marionette': False}}) + options = get_options(driver_class, request.config) + if driver_class == 'Marionette': + driver_class = 'Firefox' + options = get_options(driver_class, request.config) + if driver_class == 'Remote': + capabilities = DesiredCapabilities.FIREFOX.copy() + kwargs.update({'desired_capabilities': capabilities}) + options = get_options('Firefox', request.config) + if driver_class == 'WebKitGTK': + options = get_options(driver_class, request.config) + if driver_path is not None: + kwargs['executable_path'] = driver_path + if options is not None: + kwargs['options'] = options + driver_instance = getattr(webdriver, driver_class)(**kwargs) + yield driver_instance + if MarkEvaluator(request.node, 'no_driver_after_test').istrue(): + driver_instance = None + + +def get_options(driver_class, config): + browser_path = config.option.binary + browser_args = config.option.args + options = None + if browser_path or browser_args: + options = getattr(webdriver, '{}Options'.format(driver_class))() + if driver_class == 'WebKitGTK': + options.overlay_scrollbars_enabled = False + if browser_path is not None: + options.binary_location = browser_path + if browser_args is not None: + for arg in browser_args.split(): + options.add_argument(arg) + return options + + +@pytest.fixture(scope='session', autouse=True) +def stop_driver(request): + def fin(): + global driver_instance + if driver_instance is not None: + driver_instance.quit() + driver_instance = None + request.addfinalizer(fin) + + +def pytest_exception_interact(node, call, report): + if report.failed: + global driver_instance + if driver_instance is not None: + driver_instance.quit() + driver_instance = None + + +@pytest.fixture +def pages(driver, webserver): + class Pages(object): + def url(self, name): + return webserver.where_is(name) + + def load(self, name): + driver.get(self.url(name)) + return Pages() + + +@pytest.fixture(autouse=True, scope='session') +def server(request): + drivers = request.config.getoption('drivers') + if drivers is None or 'Remote' not in drivers: + yield None + return + + _host = 'localhost' + _port = 4444 + _path = '../buck-out/gen/java/server/src/org/openqa/grid/selenium/selenium.jar' + + def wait_for_server(url, timeout): + start = time.time() + while time.time() - start < timeout: + try: + urlopen(url) + return 1 + except IOError: + time.sleep(0.2) + return 0 + + _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + url = 'http://{}:{}/wd/hub'.format(_host, _port) + try: + _socket.connect((_host, _port)) + print('The remote driver server is already running or something else' + 'is using port {}, continuing...'.format(_port)) + except Exception: + print('Starting the Selenium server') + process = subprocess.Popen(['java', '-jar', _path]) + print('Selenium server running as process: {}'.format(process.pid)) + assert wait_for_server(url, 10), 'Timed out waiting for Selenium server at {}'.format(url) + print('Selenium server is ready') + yield process + process.terminate() + process.wait() + print('Selenium server has been terminated') + + +@pytest.fixture(autouse=True, scope='session') +def webserver(): + webserver = SimpleWebServer(host=get_lan_ip()) + webserver.start() + yield webserver + webserver.stop() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..2d483706 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,131 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +HTML_DESTINATION = ../../docs/api/py + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: clean + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(HTML_DESTINATION) + @echo + @echo "Build finished. The HTML pages are in $(HTML_DESTINATION)." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Selenium.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Selenium.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Selenium" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Selenium" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 00000000..82f128e2 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,149 @@ +:orphan: + +====================== +Selenium Documentation +====================== + +Common +------ + +.. currentmodule:: selenium.common +.. autosummary:: + :toctree: common + + selenium.common.exceptions + +Webdriver.common +---------------- + +.. currentmodule:: selenium.webdriver.common +.. autosummary:: + :toctree: webdriver + + selenium.webdriver.common.action_chains + selenium.webdriver.common.alert + selenium.webdriver.common.by + selenium.webdriver.common.desired_capabilities + selenium.webdriver.common.keys + selenium.webdriver.common.touch_actions + selenium.webdriver.common.utils + selenium.webdriver.common.proxy + selenium.webdriver.common.service + selenium.webdriver.common.html5.application_cache + +Webdriver.support +----------------- + +.. currentmodule:: selenium.webdriver.support +.. autosummary:: + :toctree: webdriver_support + + selenium.webdriver.support.abstract_event_listener + selenium.webdriver.support.color + selenium.webdriver.support.event_firing_webdriver + selenium.webdriver.support.expected_conditions + selenium.webdriver.support.select + selenium.webdriver.support.wait + +Webdriver.android +----------------- + +.. currentmodule:: selenium.webdriver.android +.. autosummary:: + :toctree: webdriver_android + + selenium.webdriver.android.webdriver + +Webdriver.chrome +---------------- + +.. currentmodule:: selenium.webdriver.chrome +.. autosummary:: + :toctree: webdriver_chrome + + selenium.webdriver.chrome.options + selenium.webdriver.chrome.service + selenium.webdriver.chrome.webdriver + +Webdriver.firefox +----------------- + +.. currentmodule:: selenium.webdriver.firefox +.. autosummary:: + :toctree: webdriver_firefox + + selenium.webdriver.firefox.extension_connection + selenium.webdriver.firefox.firefox_binary + selenium.webdriver.firefox.options + selenium.webdriver.firefox.firefox_profile + selenium.webdriver.firefox.webdriver + +Webdriver.ie +------------ + +.. currentmodule:: selenium.webdriver.ie +.. autosummary:: + :toctree: webdriver_ie + + selenium.webdriver.ie.webdriver + +Webdriver.opera +--------------- + +.. currentmodule:: selenium.webdriver.opera +.. autosummary:: + :toctree: webdriver_opera + + selenium.webdriver.opera.webdriver + +Webdriver.phantomjs +------------------- + +.. currentmodule:: selenium.webdriver.phantomjs +.. autosummary:: + :toctree: webdriver_phantomjs + + selenium.webdriver.phantomjs.service + selenium.webdriver.phantomjs.webdriver + +Webdriver.remote +---------------- + +.. currentmodule:: selenium.webdriver.remote +.. autosummary:: + :toctree: webdriver_remote + + selenium.webdriver.remote.command + selenium.webdriver.remote.errorhandler + selenium.webdriver.remote.mobile + selenium.webdriver.remote.remote_connection + selenium.webdriver.remote.utils + selenium.webdriver.remote.webdriver + selenium.webdriver.remote.webelement + +Webdriver.safari +---------------- + +.. currentmodule:: selenium.webdriver.safari +.. autosummary:: + :toctree: webdriver_safari + + selenium.webdriver.safari.service + selenium.webdriver.safari.webdriver + +Webdriver.webkitgtk +------------------- + +.. currentmodule:: selenium.webdriver.webkitgtk +.. autosummary:: + :toctree: webdriver_webkitgtk + + selenium.webdriver.webkitgtk.options + selenium.webdriver.webkitgtk.service + selenium.webdriver.webkitgtk.webdriver + +Indices and tables + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/common/selenium.common.exceptions.rst b/docs/source/common/selenium.common.exceptions.rst new file mode 100644 index 00000000..356d256c --- /dev/null +++ b/docs/source/common/selenium.common.exceptions.rst @@ -0,0 +1,4 @@ +selenium.common.exceptions +========================== + +.. automodule:: selenium.common.exceptions diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..992ea3b0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,274 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import unicode_literals + +import sys, os, os.path + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.join(os.getcwd(), "..", "..")) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'Selenium' +copyright = '2011, plightbo, simon.m.stewart, hbchai, jrhuggins, et al.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '3.141' +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Seleniumdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'Selenium.tex', 'Selenium Documentation', + 'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'selenium', 'Selenium Documentation', + ['plightbo, simon.m.stewart, hbchai, jrhuggins, et al.'], 1) +] + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = 'Selenium' +epub_author = 'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.' +epub_publisher = 'plightbo, simon.m.stewart, hbchai, jrhuggins, et al.' +epub_copyright = '2011, plightbo, simon.m.stewart, hbchai, jrhuggins, et al.' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} + +# 'members' includes anything that has a docstring, 'undoc-members' includes +# functions without docstrings. +autodoc_default_flags = ['members', 'undoc-members'] + +# Include __init__ comments +autoclass_content = "both" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..8ef805d2 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,145 @@ +====================== +Selenium Client Driver +====================== + +Introduction +============ + +Python language bindings for Selenium WebDriver. + +The `selenium` package is used to automate web browser interaction from Python. + ++-----------+--------------------------------------------------------------------------------------+ +| **Home**: | http://www.seleniumhq.org | ++-----------+--------------------------------------------------------------------------------------+ +| **Docs**: | `selenium package API <https://seleniumhq.github.io/selenium/docs/api/py/api.html>`_ | ++-----------+--------------------------------------------------------------------------------------+ +| **Dev**: | https://github.com/SeleniumHQ/Selenium | ++-----------+--------------------------------------------------------------------------------------+ +| **PyPI**: | https://pypi.org/project/selenium/ | ++-----------+--------------------------------------------------------------------------------------+ +| **IRC**: | **#selenium** channel on freenode | ++-----------+--------------------------------------------------------------------------------------+ + +Several browsers/drivers are supported (Firefox, Chrome, Internet Explorer), as well as the Remote protocol. + +Supported Python Versions +========================= + +* Python 2.7, 3.4+ + +Installing +========== + +If you have `pip <https://pip.pypa.io/>`_ on your system, you can simply install or upgrade the Python bindings:: + + pip install -U selenium + +Alternately, you can download the source distribution from `PyPI <https://pypi.org/project/selenium/#files>`_ (e.g. selenium-4.0.0a1.tar.gz), unarchive it, and run:: + + python setup.py install + +Note: You may want to consider using `virtualenv <http://www.virtualenv.org/>`_ to create isolated Python environments. + +Drivers +======= + +Selenium requires a driver to interface with the chosen browser. Firefox, +for example, requires `geckodriver <https://github.com/mozilla/geckodriver/releases>`_, which needs to be installed before the below examples can be run. Make sure it's in your `PATH`, e. g., place it in `/usr/bin` or `/usr/local/bin`. + +Failure to observe this step will give you an error `selenium.common.exceptions.WebDriverException: Message: 'geckodriver' executable needs to be in PATH.` + +Other supported browsers will have their own drivers available. Links to some of the more popular browser drivers follow. + ++--------------+-----------------------------------------------------------------------+ +| **Chrome**: | https://sites.google.com/a/chromium.org/chromedriver/downloads | ++--------------+-----------------------------------------------------------------------+ +| **Edge**: | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ | ++--------------+-----------------------------------------------------------------------+ +| **Firefox**: | https://github.com/mozilla/geckodriver/releases | ++--------------+-----------------------------------------------------------------------+ +| **Safari**: | https://webkit.org/blog/6900/webdriver-support-in-safari-10/ | ++--------------+-----------------------------------------------------------------------+ + +Example 0: +========== + +* open a new Firefox browser +* load the page at the given URL + +.. code-block:: python + + from selenium import webdriver + + browser = webdriver.Firefox() + browser.get('http://seleniumhq.org/') + +Example 1: +========== + +* open a new Firefox browser +* load the Yahoo homepage +* search for "seleniumhq" +* close the browser + +.. code-block:: python + + from selenium import webdriver + from selenium.webdriver.common.keys import Keys + + browser = webdriver.Firefox() + + browser.get('http://www.yahoo.com') + assert 'Yahoo' in browser.title + + elem = browser.find_element_by_name('p') # Find the search box + elem.send_keys('seleniumhq' + Keys.RETURN) + + browser.quit() + +Example 2: +========== + +Selenium WebDriver is often used as a basis for testing web applications. Here is a simple example using Python's standard `unittest <http://docs.python.org/3/library/unittest.html>`_ library: + +.. code-block:: python + + import unittest + from selenium import webdriver + + class GoogleTestCase(unittest.TestCase): + + def setUp(self): + self.browser = webdriver.Firefox() + self.addCleanup(self.browser.quit) + + def testPageTitle(self): + self.browser.get('http://www.google.com') + self.assertIn('Google', self.browser.title) + + if __name__ == '__main__': + unittest.main(verbosity=2) + +Selenium Server (optional) +========================== + +For normal WebDriver scripts (non-Remote), the Java server is not needed. + +However, to use Selenium Webdriver Remote or the legacy Selenium API (Selenium-RC), you need to also run the Selenium server. The server requires a Java Runtime Environment (JRE). + +Download the server separately, from: http://selenium-release.storage.googleapis.com/4.0/selenium-server-standalone-4.0.0.jar + +Run the server from the command line:: + + java -jar selenium-server-standalone-3.141.0.jar + +Then run your Python client scripts. + +Use The Source Luke! +==================== + +View source code online: + ++-----------+-------------------------------------------------------+ +| official: | https://github.com/SeleniumHQ/selenium/tree/master/py | ++-----------+-------------------------------------------------------+ diff --git a/docs/source/webdriver/selenium.webdriver.common.action_chains.rst b/docs/source/webdriver/selenium.webdriver.common.action_chains.rst new file mode 100644 index 00000000..07fff7b0 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.action_chains.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.action_chains +======================================= + +.. automodule:: selenium.webdriver.common.action_chains diff --git a/docs/source/webdriver/selenium.webdriver.common.alert.rst b/docs/source/webdriver/selenium.webdriver.common.alert.rst new file mode 100644 index 00000000..ac884381 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.alert.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.alert +=============================== + +.. automodule:: selenium.webdriver.common.alert diff --git a/docs/source/webdriver/selenium.webdriver.common.by.rst b/docs/source/webdriver/selenium.webdriver.common.by.rst new file mode 100644 index 00000000..cdf69417 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.by.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.by +============================ + +.. automodule:: selenium.webdriver.common.by diff --git a/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst b/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst new file mode 100644 index 00000000..30fdf17c --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.desired_capabilities.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.desired_capabilities +============================================== + +.. automodule:: selenium.webdriver.common.desired_capabilities diff --git a/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst b/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst new file mode 100644 index 00000000..e2e06394 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.html5.application_cache.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.html5.application_cache +==================================================== + +.. automodule:: selenium.webdriver.common.html5.application_cache diff --git a/docs/source/webdriver/selenium.webdriver.common.keys.rst b/docs/source/webdriver/selenium.webdriver.common.keys.rst new file mode 100644 index 00000000..1704a17c --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.keys.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.keys +============================== + +.. automodule:: selenium.webdriver.common.keys diff --git a/docs/source/webdriver/selenium.webdriver.common.proxy.rst b/docs/source/webdriver/selenium.webdriver.common.proxy.rst new file mode 100644 index 00000000..06a77f1b --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.proxy.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.proxy +=============================== + +.. automodule:: selenium.webdriver.common.proxy diff --git a/docs/source/webdriver/selenium.webdriver.common.service.rst b/docs/source/webdriver/selenium.webdriver.common.service.rst new file mode 100644 index 00000000..e313ced3 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.service.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.service +================================= + +.. automodule:: selenium.webdriver.common.service diff --git a/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst b/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst new file mode 100644 index 00000000..7bc9aa64 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.touch_actions.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.touch_actions +======================================= + +.. automodule:: selenium.webdriver.common.touch_actions diff --git a/docs/source/webdriver/selenium.webdriver.common.utils.rst b/docs/source/webdriver/selenium.webdriver.common.utils.rst new file mode 100644 index 00000000..f30250a6 --- /dev/null +++ b/docs/source/webdriver/selenium.webdriver.common.utils.rst @@ -0,0 +1,4 @@ +selenium.webdriver.common.utils +=============================== + +.. automodule:: selenium.webdriver.common.utils diff --git a/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst b/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst new file mode 100644 index 00000000..a7693d9d --- /dev/null +++ b/docs/source/webdriver_android/selenium.webdriver.android.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.android.webdriver +==================================== + +.. automodule:: selenium.webdriver.android.webdriver diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst new file mode 100644 index 00000000..9a6e42a9 --- /dev/null +++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.options.rst @@ -0,0 +1,4 @@ +selenium.webdriver.chrome.options +================================= + +.. automodule:: selenium.webdriver.chrome.options diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst new file mode 100644 index 00000000..ffe8b20f --- /dev/null +++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.service.rst @@ -0,0 +1,4 @@ +selenium.webdriver.chrome.service +================================= + +.. automodule:: selenium.webdriver.chrome.service diff --git a/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst b/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst new file mode 100644 index 00000000..7f542536 --- /dev/null +++ b/docs/source/webdriver_chrome/selenium.webdriver.chrome.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.chrome.webdriver +=================================== + +.. automodule:: selenium.webdriver.chrome.webdriver diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst new file mode 100644 index 00000000..413badac --- /dev/null +++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.extension_connection.rst @@ -0,0 +1,4 @@ +selenium.webdriver.firefox.extension_connection +=============================================== + +.. automodule:: selenium.webdriver.firefox.extension_connection diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst new file mode 100644 index 00000000..e0b04b18 --- /dev/null +++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_binary.rst @@ -0,0 +1,4 @@ +selenium.webdriver.firefox.firefox_binary +========================================= + +.. automodule:: selenium.webdriver.firefox.firefox_binary diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst new file mode 100644 index 00000000..78ba9499 --- /dev/null +++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.rst @@ -0,0 +1,4 @@ +selenium.webdriver.firefox.firefox_profile +========================================== + +.. automodule:: selenium.webdriver.firefox.firefox_profile diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst new file mode 100644 index 00000000..a64b990a --- /dev/null +++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.options.rst @@ -0,0 +1,4 @@ +selenium.webdriver.firefox.options +================================== + +.. automodule:: selenium.webdriver.firefox.options diff --git a/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst b/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst new file mode 100644 index 00000000..e9bfe53e --- /dev/null +++ b/docs/source/webdriver_firefox/selenium.webdriver.firefox.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.firefox.webdriver +==================================== + +.. automodule:: selenium.webdriver.firefox.webdriver diff --git a/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst b/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst new file mode 100644 index 00000000..3623813c --- /dev/null +++ b/docs/source/webdriver_ie/selenium.webdriver.ie.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.ie.webdriver +=============================== + +.. automodule:: selenium.webdriver.ie.webdriver diff --git a/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst b/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst new file mode 100644 index 00000000..ddabbc36 --- /dev/null +++ b/docs/source/webdriver_opera/selenium.webdriver.opera.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.opera.webdriver +================================== + +.. automodule:: selenium.webdriver.opera.webdriver diff --git a/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst new file mode 100644 index 00000000..8d6e5ff7 --- /dev/null +++ b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.service.rst @@ -0,0 +1,4 @@ +selenium.webdriver.phantomjs.service +==================================== + +.. automodule:: selenium.webdriver.phantomjs.service diff --git a/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst new file mode 100644 index 00000000..5b3ab482 --- /dev/null +++ b/docs/source/webdriver_phantomjs/selenium.webdriver.phantomjs.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.phantomjs.webdriver +====================================== + +.. automodule:: selenium.webdriver.phantomjs.webdriver diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst new file mode 100644 index 00000000..ff977b74 --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.command.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.command +================================= + +.. automodule:: selenium.webdriver.remote.command diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst new file mode 100644 index 00000000..50028874 --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.errorhandler.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.errorhandler +====================================== + +.. automodule:: selenium.webdriver.remote.errorhandler diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst new file mode 100644 index 00000000..dc6ab4ab --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.mobile.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.mobile +================================ + +.. automodule:: selenium.webdriver.remote.mobile diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst new file mode 100644 index 00000000..88d13894 --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.remote_connection.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.remote_connection +=========================================== + +.. automodule:: selenium.webdriver.remote.remote_connection diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst new file mode 100644 index 00000000..e035f43a --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.utils.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.utils +=============================== + +.. automodule:: selenium.webdriver.remote.utils diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst new file mode 100644 index 00000000..b390c196 --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.webdriver +=================================== + +.. automodule:: selenium.webdriver.remote.webdriver diff --git a/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst b/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst new file mode 100644 index 00000000..7b6ab4de --- /dev/null +++ b/docs/source/webdriver_remote/selenium.webdriver.remote.webelement.rst @@ -0,0 +1,4 @@ +selenium.webdriver.remote.webelement +==================================== + +.. automodule:: selenium.webdriver.remote.webelement diff --git a/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst b/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst new file mode 100644 index 00000000..de44909b --- /dev/null +++ b/docs/source/webdriver_safari/selenium.webdriver.safari.service.rst @@ -0,0 +1,4 @@ +selenium.webdriver.safari.service +================================= + +.. automodule:: selenium.webdriver.safari.service diff --git a/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst b/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst new file mode 100644 index 00000000..f3b649ed --- /dev/null +++ b/docs/source/webdriver_safari/selenium.webdriver.safari.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.safari.webdriver +=================================== + +.. automodule:: selenium.webdriver.safari.webdriver diff --git a/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst b/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst new file mode 100644 index 00000000..53828ad5 --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.abstract_event_listener.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.abstract_event_listener +================================================== + +.. automodule:: selenium.webdriver.support.abstract_event_listener diff --git a/docs/source/webdriver_support/selenium.webdriver.support.color.rst b/docs/source/webdriver_support/selenium.webdriver.support.color.rst new file mode 100644 index 00000000..44aa61cf --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.color.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.color +================================ + +.. automodule:: selenium.webdriver.support.color diff --git a/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst b/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst new file mode 100644 index 00000000..abbeacd0 --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.event_firing_webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.event_firing_webdriver +================================================= + +.. automodule:: selenium.webdriver.support.event_firing_webdriver diff --git a/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst b/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst new file mode 100644 index 00000000..46417a9b --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.expected_conditions.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.expected_conditions +============================================== + +.. automodule:: selenium.webdriver.support.expected_conditions diff --git a/docs/source/webdriver_support/selenium.webdriver.support.select.rst b/docs/source/webdriver_support/selenium.webdriver.support.select.rst new file mode 100644 index 00000000..02b0eb0b --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.select.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.select +================================= + +.. automodule:: selenium.webdriver.support.select diff --git a/docs/source/webdriver_support/selenium.webdriver.support.wait.rst b/docs/source/webdriver_support/selenium.webdriver.support.wait.rst new file mode 100644 index 00000000..f42089be --- /dev/null +++ b/docs/source/webdriver_support/selenium.webdriver.support.wait.rst @@ -0,0 +1,4 @@ +selenium.webdriver.support.wait +=============================== + +.. automodule:: selenium.webdriver.support.wait diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst new file mode 100644 index 00000000..192eb79d --- /dev/null +++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.options.rst @@ -0,0 +1,4 @@ +selenium.webdriver.webkitgtk.options +==================================== + +.. automodule:: selenium.webdriver.webkitgtk.options diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst new file mode 100644 index 00000000..81497e01 --- /dev/null +++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.service.rst @@ -0,0 +1,4 @@ +selenium.webdriver.webkitgtk.service +==================================== + +.. automodule:: selenium.webdriver.webkitgtk.service diff --git a/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst new file mode 100644 index 00000000..c819a4a9 --- /dev/null +++ b/docs/source/webdriver_webkitgtk/selenium.webdriver.webkitgtk.webdriver.rst @@ -0,0 +1,4 @@ +selenium.webdriver.webkitgtk.webdriver +====================================== + +.. automodule:: selenium.webdriver.webkitgtk.webdriver diff --git a/python.iml b/python.iml new file mode 100644 index 00000000..3c8cbb09 --- /dev/null +++ b/python.iml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="Python" name="Python"> + <configuration sdkName="" /> + </facet> + </component> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/selenium" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> + </content> + <orderEntry type="jdk" jdkName="Python 2.7" jdkType="Python SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="sonarModuleSettings"> + <option name="alternativeWorkingDirPath" value="" /> + <option name="localAnalysisScripName" value="<PROJECT>" /> + <option name="serverName" value="<PROJECT>" /> + <option name="useAlternativeWorkingDir" value="false" /> + <option name="workingDirSelection" value="<MODULE>" /> + </component> +</module> \ No newline at end of file diff --git a/selenium/__init__.py b/selenium/__init__.py new file mode 100644 index 00000000..c4a089da --- /dev/null +++ b/selenium/__init__.py @@ -0,0 +1,19 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +__version__ = "4.0.0a1" diff --git a/selenium/common/__init__.py b/selenium/common/__init__.py new file mode 100644 index 00000000..ea71319a --- /dev/null +++ b/selenium/common/__init__.py @@ -0,0 +1,18 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from . import exceptions # noqa diff --git a/selenium/common/exceptions.py b/selenium/common/exceptions.py new file mode 100644 index 00000000..1b151216 --- /dev/null +++ b/selenium/common/exceptions.py @@ -0,0 +1,315 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Exceptions that may happen in all the webdriver code. +""" + + +class WebDriverException(Exception): + """ + Base webdriver exception. + """ + + def __init__(self, msg=None, screen=None, stacktrace=None): + self.msg = msg + self.screen = screen + self.stacktrace = stacktrace + + def __str__(self): + exception_msg = "Message: %s\n" % self.msg + if self.screen is not None: + exception_msg += "Screenshot: available via screen\n" + if self.stacktrace is not None: + stacktrace = "\n".join(self.stacktrace) + exception_msg += "Stacktrace:\n%s" % stacktrace + return exception_msg + + +class InvalidSwitchToTargetException(WebDriverException): + """ + Thrown when frame or window target to be switched doesn't exist. + """ + pass + + +class NoSuchFrameException(InvalidSwitchToTargetException): + """ + Thrown when frame target to be switched doesn't exist. + """ + pass + + +class NoSuchWindowException(InvalidSwitchToTargetException): + """ + Thrown when window target to be switched doesn't exist. + + To find the current set of active window handles, you can get a list + of the active window handles in the following way:: + + print driver.window_handles + + """ + pass + + +class NoSuchElementException(WebDriverException): + """ + Thrown when element could not be found. + + If you encounter this exception, you may want to check the following: + * Check your selector used in your find_by... + * Element may not yet be on the screen at the time of the find operation, + (webpage is still loading) see selenium.webdriver.support.wait.WebDriverWait() + for how to write a wait wrapper to wait for an element to appear. + """ + pass + + +class NoSuchAttributeException(WebDriverException): + """ + Thrown when the attribute of element could not be found. + + You may want to check if the attribute exists in the particular browser you are + testing against. Some browsers may have different property names for the same + property. (IE8's .innerText vs. Firefox .textContent) + """ + pass + + +class StaleElementReferenceException(WebDriverException): + """ + Thrown when a reference to an element is now "stale". + + Stale means the element no longer appears on the DOM of the page. + + + Possible causes of StaleElementReferenceException include, but not limited to: + * You are no longer on the same page, or the page may have refreshed since the element + was located. + * The element may have been removed and re-added to the screen, since it was located. + Such as an element being relocated. + This can happen typically with a javascript framework when values are updated and the + node is rebuilt. + * Element may have been inside an iframe or another context which was refreshed. + """ + pass + + +class InvalidElementStateException(WebDriverException): + """ + Thrown when a command could not be completed because the element is in an invalid state. + + This can be caused by attempting to clear an element that isn't both editable and resettable. + """ + pass + + +class UnexpectedAlertPresentException(WebDriverException): + """ + Thrown when an unexpected alert has appeared. + + Usually raised when an unexpected modal is blocking the webdriver from executing + commands. + """ + def __init__(self, msg=None, screen=None, stacktrace=None, alert_text=None): + super(UnexpectedAlertPresentException, self).__init__(msg, screen, stacktrace) + self.alert_text = alert_text + + def __str__(self): + return "Alert Text: %s\n%s" % (self.alert_text, super(UnexpectedAlertPresentException, self).__str__()) + + +class NoAlertPresentException(WebDriverException): + """ + Thrown when switching to no presented alert. + + This can be caused by calling an operation on the Alert() class when an alert is + not yet on the screen. + """ + pass + + +class ElementNotVisibleException(InvalidElementStateException): + """ + Thrown when an element is present on the DOM, but + it is not visible, and so is not able to be interacted with. + + Most commonly encountered when trying to click or read text + of an element that is hidden from view. + """ + pass + + +class ElementNotInteractableException(InvalidElementStateException): + """ + Thrown when an element is present in the DOM but interactions + with that element will hit another element do to paint order + """ + pass + + +class ElementNotSelectableException(InvalidElementStateException): + """ + Thrown when trying to select an unselectable element. + + For example, selecting a 'script' element. + """ + pass + + +class InvalidCookieDomainException(WebDriverException): + """ + Thrown when attempting to add a cookie under a different domain + than the current URL. + """ + pass + + +class UnableToSetCookieException(WebDriverException): + """ + Thrown when a driver fails to set a cookie. + """ + pass + + +class RemoteDriverServerException(WebDriverException): + """ + """ + pass + + +class TimeoutException(WebDriverException): + """ + Thrown when a command does not complete in enough time. + """ + pass + + +class MoveTargetOutOfBoundsException(WebDriverException): + """ + Thrown when the target provided to the `ActionsChains` move() + method is invalid, i.e. out of document. + """ + pass + + +class UnexpectedTagNameException(WebDriverException): + """ + Thrown when a support class did not get an expected web element. + """ + pass + + +class InvalidSelectorException(NoSuchElementException): + """ + Thrown when the selector which is used to find an element does not return + a WebElement. Currently this only happens when the selector is an xpath + expression and it is either syntactically invalid (i.e. it is not a + xpath expression) or the expression does not select WebElements + (e.g. "count(//input)"). + """ + pass + + +class ImeNotAvailableException(WebDriverException): + """ + Thrown when IME support is not available. This exception is thrown for every IME-related + method call if IME support is not available on the machine. + """ + pass + + +class ImeActivationFailedException(WebDriverException): + """ + Thrown when activating an IME engine has failed. + """ + pass + + +class InvalidArgumentException(WebDriverException): + """ + The arguments passed to a command are either invalid or malformed. + """ + pass + + +class JavascriptException(WebDriverException): + """ + An error occurred while executing JavaScript supplied by the user. + """ + pass + + +class NoSuchCookieException(WebDriverException): + """ + No cookie matching the given path name was found amongst the associated cookies of the + current browsing context's active document. + """ + pass + + +class ScreenshotException(WebDriverException): + """ + A screen capture was made impossible. + """ + pass + + +class ElementClickInterceptedException(WebDriverException): + """ + The Element Click command could not be completed because the element receiving the events + is obscuring the element that was requested clicked. + """ + pass + + +class InsecureCertificateException(WebDriverException): + """ + Navigation caused the user agent to hit a certificate warning, which is usually the result + of an expired or invalid TLS certificate. + """ + pass + + +class InvalidCoordinatesException(WebDriverException): + """ + The coordinates provided to an interactions operation are invalid. + """ + pass + + +class InvalidSessionIdException(WebDriverException): + """ + Occurs if the given session id is not in the list of active sessions, meaning the session + either does not exist or that it's not active. + """ + pass + + +class SessionNotCreatedException(WebDriverException): + """ + A new session could not be created. + """ + pass + + +class UnknownMethodException(WebDriverException): + """ + The requested command matched a known URL but did not match an method for that URL. + """ + pass diff --git a/selenium/webdriver/__init__.py b/selenium/webdriver/__init__.py new file mode 100644 index 00000000..b0f17911 --- /dev/null +++ b/selenium/webdriver/__init__.py @@ -0,0 +1,39 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from .firefox.webdriver import WebDriver as Firefox # noqa +from .firefox.firefox_profile import FirefoxProfile # noqa +from .firefox.options import Options as FirefoxOptions # noqa +from .chrome.webdriver import WebDriver as Chrome # noqa +from .chrome.options import Options as ChromeOptions # noqa +from .ie.webdriver import WebDriver as Ie # noqa +from .ie.options import Options as IeOptions # noqa +from .edge.webdriver import WebDriver as Edge # noqa +from .opera.webdriver import WebDriver as Opera # noqa +from .safari.webdriver import WebDriver as Safari # noqa +from .blackberry.webdriver import WebDriver as BlackBerry # noqa +from .phantomjs.webdriver import WebDriver as PhantomJS # noqa +from .android.webdriver import WebDriver as Android # noqa +from .webkitgtk.webdriver import WebDriver as WebKitGTK # noqa +from .webkitgtk.options import Options as WebKitGTKOptions # noqa +from .remote.webdriver import WebDriver as Remote # noqa +from .common.desired_capabilities import DesiredCapabilities # noqa +from .common.action_chains import ActionChains # noqa +from .common.touch_actions import TouchActions # noqa +from .common.proxy import Proxy # noqa + +__version__ = '4.0.0a1' diff --git a/selenium/webdriver/android/__init__.py b/selenium/webdriver/android/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/android/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/android/webdriver.py b/selenium/webdriver/android/webdriver.py new file mode 100644 index 00000000..68a55c27 --- /dev/null +++ b/selenium/webdriver/android/webdriver.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class WebDriver(RemoteWebDriver): + """ + Simple RemoteWebDriver wrapper to start connect to Selendroid's WebView app + + For more info on getting started with Selendroid + http://selendroid.io/mobileWeb.html + """ + + def __init__(self, host="localhost", port=4444, desired_capabilities=DesiredCapabilities.ANDROID): + """ + Creates a new instance of Selendroid using the WebView app + + :Args: + - host - location of where selendroid is running + - port - port that selendroid is running on + - desired_capabilities: Dictionary object with capabilities + """ + RemoteWebDriver.__init__( + self, + command_executor="http://%s:%d/wd/hub" % (host, port), + desired_capabilities=desired_capabilities) diff --git a/selenium/webdriver/blackberry/__init__.py b/selenium/webdriver/blackberry/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/blackberry/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/blackberry/webdriver.py b/selenium/webdriver/blackberry/webdriver.py new file mode 100644 index 00000000..cded6e7d --- /dev/null +++ b/selenium/webdriver/blackberry/webdriver.py @@ -0,0 +1,121 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +import platform +import subprocess + +try: + import http.client as http_client +except ImportError: + import httplib as http_client + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support.ui import WebDriverWait + +LOAD_TIMEOUT = 5 + + +class WebDriver(RemoteWebDriver): + """ + Controls the BlackBerry Browser and allows you to drive it. + + :Args: + - device_password - password for the BlackBerry device or emulator you are + trying to drive + - bb_tools_dir path to the blackberry-deploy executable. If the default + is used it assumes it is in the $PATH + - hostip - the ip for the device you are trying to drive. Falls back to + 169.254.0.1 which is the default ip used + - port - the port being used for WebDriver on device. defaults to 1338 + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + + Note: To get blackberry-deploy you will need to install the BlackBerry + WebWorks SDK - the default install will put it in the $PATH for you. + Download at https://developer.blackberry.com/html5/downloads/ + """ + def __init__(self, device_password, bb_tools_dir=None, + hostip='169.254.0.1', port=1338, desired_capabilities={}): + import warnings + warnings.warn('BlackBerry Driver is no longer supported and will be ' + 'removed in future versions', + DeprecationWarning, stacklevel=2) + + remote_addr = 'http://{}:{}'.format(hostip, port) + + filename = 'blackberry-deploy' + if platform.system() == "Windows": + filename += '.bat' + + if bb_tools_dir is not None: + if os.path.isdir(bb_tools_dir): + bb_deploy_location = os.path.join(bb_tools_dir, filename) + if not os.path.isfile(bb_deploy_location): + raise WebDriverException('Invalid blackberry-deploy location: {}'.format(bb_deploy_location)) + else: + raise WebDriverException('Invalid blackberry tools location, must be a directory: {}'.format(bb_tools_dir)) + else: + bb_deploy_location = filename + + """ + Now launch the BlackBerry browser before allowing anything else to run. + """ + try: + launch_args = [bb_deploy_location, + '-launchApp', + str(hostip), + '-package-name', 'sys.browser', + '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0', + '-password', str(device_password)] + + with open(os.devnull, 'w') as fp: + p = subprocess.Popen(launch_args, stdout=fp) + + returncode = p.wait() + + if returncode == 0: + # wait for the BlackBerry10 browser to load. + is_running_args = [bb_deploy_location, + '-isAppRunning', + str(hostip), + '-package-name', 'sys.browser', + '-package-id', 'gYABgJYFHAzbeFMPCCpYWBtHAm0', + '-password', str(device_password)] + + WebDriverWait(None, LOAD_TIMEOUT)\ + .until(lambda x: subprocess.check_output(is_running_args) + .find('result::true'), + message='waiting for BlackBerry10 browser to load') + + RemoteWebDriver.__init__(self, + command_executor=remote_addr, + desired_capabilities=desired_capabilities) + else: + raise WebDriverException('blackberry-deploy failed to launch browser') + except Exception as e: + raise WebDriverException('Something went wrong launching blackberry-deploy', stacktrace=getattr(e, 'stacktrace', None)) + + def quit(self): + """ + Closes the browser and shuts down the + """ + try: + RemoteWebDriver.quit(self) + except http_client.BadStatusLine: + pass diff --git a/selenium/webdriver/chrome/__init__.py b/selenium/webdriver/chrome/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/chrome/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/chrome/options.py b/selenium/webdriver/chrome/options.py new file mode 100644 index 00000000..c5ec4180 --- /dev/null +++ b/selenium/webdriver/chrome/options.py @@ -0,0 +1,177 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import os + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.options import ArgOptions + + +class Options(ArgOptions): + KEY = "goog:chromeOptions" + + def __init__(self): + super(Options, self).__init__() + self._binary_location = '' + self._extension_files = [] + self._extensions = [] + self._experimental_options = {} + self._debugger_address = None + + @property + def binary_location(self): + """ + :Returns: The location of the binary, otherwise an empty string + """ + return self._binary_location + + @binary_location.setter + def binary_location(self, value): + """ + Allows you to set where the chromium binary lives + + :Args: + - value: path to the Chromium binary + """ + self._binary_location = value + + @property + def debugger_address(self): + """ + :Returns: The address of the remote devtools instance + """ + return self._debugger_address + + @debugger_address.setter + def debugger_address(self, value): + """ + Allows you to set the address of the remote devtools instance + that the ChromeDriver instance will try to connect to during an + active wait. + + :Args: + - value: address of remote devtools instance if any (hostname[:port]) + """ + self._debugger_address = value + + @property + def extensions(self): + """ + :Returns: A list of encoded extensions that will be loaded into chrome + """ + encoded_extensions = [] + for ext in self._extension_files: + file_ = open(ext, 'rb') + # Should not use base64.encodestring() which inserts newlines every + # 76 characters (per RFC 1521). Chromedriver has to remove those + # unnecessary newlines before decoding, causing performance hit. + encoded_extensions.append(base64.b64encode(file_.read()).decode('UTF-8')) + + file_.close() + return encoded_extensions + self._extensions + + def add_extension(self, extension): + """ + Adds the path to the extension to a list that will be used to extract it + to the ChromeDriver + + :Args: + - extension: path to the \\*.crx file + """ + if extension: + extension_to_add = os.path.abspath(os.path.expanduser(extension)) + if os.path.exists(extension_to_add): + self._extension_files.append(extension_to_add) + else: + raise IOError("Path to the extension doesn't exist") + else: + raise ValueError("argument can not be null") + + def add_encoded_extension(self, extension): + """ + Adds Base64 encoded string with extension data to a list that will be used to extract it + to the ChromeDriver + + :Args: + - extension: Base64 encoded string with extension data + """ + if extension: + self._extensions.append(extension) + else: + raise ValueError("argument can not be null") + + @property + def experimental_options(self): + """ + :Returns: A dictionary of experimental options for chrome + """ + return self._experimental_options + + def add_experimental_option(self, name, value): + """ + Adds an experimental option which is passed to chrome. + + :Args: + name: The experimental option name. + value: The option value. + """ + self._experimental_options[name] = value + + @property + def headless(self): + """ + :Returns: True if the headless argument is set, else False + """ + return '--headless' in self._arguments + + @headless.setter + def headless(self, value): + """ + Sets the headless argument + + :Args: + value: boolean value indicating to set the headless option + """ + args = {'--headless'} + if value is True: + self._arguments.extend(args) + else: + self._arguments = list(set(self._arguments) - args) + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set + + :Returns: A dictionary with everything + """ + caps = self._caps + chrome_options = self.experimental_options.copy() + chrome_options["extensions"] = self.extensions + if self.binary_location: + chrome_options["binary"] = self.binary_location + chrome_options["args"] = self.arguments + if self.debugger_address: + chrome_options["debuggerAddress"] = self.debugger_address + + caps[self.KEY] = chrome_options + + return caps + + @property + def default_capabilities(self): + return DesiredCapabilities.CHROME.copy() diff --git a/selenium/webdriver/chrome/remote_connection.py b/selenium/webdriver/chrome/remote_connection.py new file mode 100644 index 00000000..d8408a08 --- /dev/null +++ b/selenium/webdriver/chrome/remote_connection.py @@ -0,0 +1,33 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.remote_connection import RemoteConnection + + +class ChromeRemoteConnection(RemoteConnection): + + def __init__(self, remote_server_addr, keep_alive=True): + RemoteConnection.__init__(self, remote_server_addr, keep_alive) + self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app') + self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions') + self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions') + self._commands['executeCdpCommand'] = ('POST', '/session/$sessionId/goog/cdp/execute') + self._commands['getSinks'] = ('GET', '/session/$sessionId/goog/cast/get_sinks') + self._commands['getIssueMessage'] = ('GET', '/session/$sessionId/goog/cast/get_issue_message') + self._commands['setSinkToUse'] = ('POST', '/session/$sessionId/goog/cast/set_sink_to_use') + self._commands['startTabMirroring'] = ('POST', '/session/$sessionId/goog/cast/start_tab_mirroring') + self._commands['stopCasting'] = ('POST', '/session/$sessionId/goog/cast/stop_casting') diff --git a/selenium/webdriver/chrome/service.py b/selenium/webdriver/chrome/service.py new file mode 100644 index 00000000..5a67b2cb --- /dev/null +++ b/selenium/webdriver/chrome/service.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the ChromeDriver + """ + + def __init__(self, executable_path, port=0, service_args=None, + log_path=None, env=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the ChromeDriver + - port : Port the service is running on + - service_args : List of args to pass to the chromedriver service + - log_path : Path for the chromedriver service to log to""" + + self.service_args = service_args or [] + if log_path: + self.service_args.append('--log-path=%s' % log_path) + + service.Service.__init__(self, executable_path, port=port, env=env, + start_error_message="Please see https://sites.google.com/a/chromium.org/chromedriver/home") + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/selenium/webdriver/chrome/webdriver.py b/selenium/webdriver/chrome/webdriver.py new file mode 100644 index 00000000..97d12d13 --- /dev/null +++ b/selenium/webdriver/chrome/webdriver.py @@ -0,0 +1,223 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import warnings +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .remote_connection import ChromeRemoteConnection +from .service import Service +from .options import Options + + +DEFAULT_PORT = 0 +DEFAULT_SERVICE_LOG_PATH = None + + +class WebDriver(RemoteWebDriver): + """ + Controls the ChromeDriver and allows you to drive the browser. + + You will need to download the ChromeDriver executable from + http://chromedriver.storage.googleapis.com/index.html + """ + + def __init__(self, executable_path="chromedriver", port=DEFAULT_PORT, + options=None, service_args=None, + desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH, + chrome_options=None, service=None, keep_alive=True): + """ + Creates a new instance of the chrome driver. + + Starts the service and then creates new instance of chrome driver. + + :Args: + - executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH + - port - Deprecated: port you would like the service to run, if left as 0, a free port will be found. + - options - this takes an instance of ChromeOptions + - service_args - Deprecated: List of args to pass to the driver service + - desired_capabilities - Deprecated: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - service_log_path - Deprecated: Where to log information from the driver. + - keep_alive - Whether to configure ChromeRemoteConnection to use HTTP keep-alive. + """ + if executable_path != 'chromedriver': + warnings.warn('executable_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if desired_capabilities is not None: + warnings.warn('desired_capabilities has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if port != DEFAULT_PORT: + warnings.warn('port has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.port = port + if service_log_path != DEFAULT_SERVICE_LOG_PATH: + warnings.warn('service_log_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + + if chrome_options: + warnings.warn('use options instead of chrome_options', + DeprecationWarning, stacklevel=2) + options = chrome_options + + if options is None: + # desired_capabilities stays as passed in + if desired_capabilities is None: + desired_capabilities = self.create_options().to_capabilities() + else: + if desired_capabilities is None: + desired_capabilities = options.to_capabilities() + else: + desired_capabilities.update(options.to_capabilities()) + + if service: + self.service = service + else: + self.service = Service( + executable_path, + port=port, + service_args=service_args, + log_path=service_log_path) + self.service.start() + + try: + RemoteWebDriver.__init__( + self, + command_executor=ChromeRemoteConnection( + remote_server_addr=self.service.service_url, + keep_alive=keep_alive), + desired_capabilities=desired_capabilities) + except Exception: + self.quit() + raise + self._is_remote = False + + def launch_app(self, id): + """Launches Chrome app specified by id.""" + return self.execute("launchApp", {'id': id}) + + def get_network_conditions(self): + """ + Gets Chrome network emulation settings. + + :Returns: + A dict. For example: + + {'latency': 4, 'download_throughput': 2, 'upload_throughput': 2, + 'offline': False} + + """ + return self.execute("getNetworkConditions")['value'] + + def set_network_conditions(self, **network_conditions): + """ + Sets Chrome network emulation settings. + + :Args: + - network_conditions: A dict with conditions specification. + + :Usage: + :: + + driver.set_network_conditions( + offline=False, + latency=5, # additional latency (ms) + download_throughput=500 * 1024, # maximal throughput + upload_throughput=500 * 1024) # maximal throughput + + Note: 'throughput' can be used to set both (for download and upload). + """ + self.execute("setNetworkConditions", { + 'network_conditions': network_conditions + }) + + def execute_cdp_cmd(self, cmd, cmd_args): + """ + Execute Chrome Devtools Protocol command and get returned result + + The command and command args should follow chrome devtools protocol domains/commands, refer to link + https://chromedevtools.github.io/devtools-protocol/ + + :Args: + - cmd: A str, command name + - cmd_args: A dict, command args. empty dict {} if there is no command args + + :Usage: + :: + + driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId}) + + :Returns: + A dict, empty dict {} if there is no result to return. + For example to getResponseBody: + + {'base64Encoded': False, 'body': 'response body string'} + + """ + return self.execute("executeCdpCommand", {'cmd': cmd, 'params': cmd_args})['value'] + + def get_sinks(self): + """ + :Returns: A list of sinks avaliable for Cast. + """ + return self.execute('getSinks')['value'] + + def get_issue_message(self): + """ + :Returns: An error message when there is any issue in a Cast session. + """ + return self.execute('getIssueMessage')['value'] + + def set_sink_to_use(self, sink_name): + """ + Sets a specific sink, using its name, as a Cast session receiver target. + + :Args: + - sink_name: Name of the sink to use as the target. + """ + return self.execute('setSinkToUse', {'sinkName': sink_name}) + + def start_tab_mirroring(self, sink_name): + """ + Starts a tab mirroring session on a specific receiver target. + + :Args: + - sink_name: Name of the sink to use as the target. + """ + return self.execute('startTabMirroring', {'sinkName': sink_name}) + + def stop_casting(self, sink_name): + """ + Stops the existing Cast session on a specific receiver target. + + :Args: + - sink_name: Name of the sink to stop the Cast session. + """ + return self.execute('stopCasting', {'sinkName': sink_name}) + + def quit(self): + """ + Closes the browser and shuts down the ChromeDriver executable + that is started when starting the ChromeDriver + """ + try: + RemoteWebDriver.quit(self) + except Exception: + # We don't care about the message because something probably has gone wrong + pass + finally: + self.service.stop() + + def create_options(self): + return Options() diff --git a/selenium/webdriver/common/__init__.py b/selenium/webdriver/common/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/common/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/common/action_chains.py b/selenium/webdriver/common/action_chains.py new file mode 100644 index 00000000..704124bd --- /dev/null +++ b/selenium/webdriver/common/action_chains.py @@ -0,0 +1,365 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The ActionChains implementation, +""" + +import time + +from selenium.webdriver.remote.command import Command + +from .utils import keys_to_typing +from .actions.action_builder import ActionBuilder + + +class ActionChains(object): + """ + ActionChains are a way to automate low level interactions such as + mouse movements, mouse button actions, key press, and context menu interactions. + This is useful for doing more complex actions like hover over and drag and drop. + + Generate user actions. + When you call methods for actions on the ActionChains object, + the actions are stored in a queue in the ActionChains object. + When you call perform(), the events are fired in the order they + are queued up. + + ActionChains can be used in a chain pattern:: + + menu = driver.find_element_by_css_selector(".nav") + hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1") + + ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform() + + Or actions can be queued up one by one, then performed.:: + + menu = driver.find_element_by_css_selector(".nav") + hidden_submenu = driver.find_element_by_css_selector(".nav #submenu1") + + actions = ActionChains(driver) + actions.move_to_element(menu) + actions.click(hidden_submenu) + actions.perform() + + Either way, the actions are performed in the order they are called, one after + another. + """ + + def __init__(self, driver): + """ + Creates a new ActionChains. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self._driver = driver + self._actions = [] + if self._driver.w3c: + self.w3c_actions = ActionBuilder(driver) + + def perform(self): + """ + Performs all stored actions. + """ + if self._driver.w3c: + self.w3c_actions.perform() + else: + for action in self._actions: + action() + + def reset_actions(self): + """ + Clears actions that are already stored locally and on the remote end + """ + if self._driver.w3c: + self.w3c_actions.clear_actions() + for device in self.w3c_actions.devices: + device.clear_actions() + self._actions = [] + + def click(self, on_element=None): + """ + Clicks an element. + + :Args: + - on_element: The element to click. + If None, clicks on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.click() + self.w3c_actions.key_action.pause() + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.CLICK, {'button': 0})) + return self + + def click_and_hold(self, on_element=None): + """ + Holds down the left mouse button on an element. + + :Args: + - on_element: The element to mouse down. + If None, clicks on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.click_and_hold() + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.MOUSE_DOWN, {})) + return self + + def context_click(self, on_element=None): + """ + Performs a context-click (right click) on an element. + + :Args: + - on_element: The element to context-click. + If None, clicks on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.context_click() + self.w3c_actions.key_action.pause() + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.CLICK, {'button': 2})) + return self + + def double_click(self, on_element=None): + """ + Double-clicks an element. + + :Args: + - on_element: The element to double-click. + If None, clicks on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.double_click() + for _ in range(4): + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.DOUBLE_CLICK, {})) + return self + + def drag_and_drop(self, source, target): + """ + Holds down the left mouse button on the source element, + then moves to the target element and releases the mouse button. + + :Args: + - source: The element to mouse down. + - target: The element to mouse up. + """ + self.click_and_hold(source) + self.release(target) + return self + + def drag_and_drop_by_offset(self, source, xoffset, yoffset): + """ + Holds down the left mouse button on the source element, + then moves to the target offset and releases the mouse button. + + :Args: + - source: The element to mouse down. + - xoffset: X offset to move to. + - yoffset: Y offset to move to. + """ + self.click_and_hold(source) + self.move_by_offset(xoffset, yoffset) + self.release() + return self + + def key_down(self, value, element=None): + """ + Sends a key press only, without releasing it. + Should only be used with modifier keys (Control, Alt and Shift). + + :Args: + - value: The modifier key to send. Values are defined in `Keys` class. + - element: The element to send keys. + If None, sends a key to current focused element. + + Example, pressing ctrl+c:: + + ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform() + + """ + if element: + self.click(element) + if self._driver.w3c: + self.w3c_actions.key_action.key_down(value) + self.w3c_actions.pointer_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, + {"value": keys_to_typing(value)})) + return self + + def key_up(self, value, element=None): + """ + Releases a modifier key. + + :Args: + - value: The modifier key to send. Values are defined in Keys class. + - element: The element to send keys. + If None, sends a key to current focused element. + + Example, pressing ctrl+c:: + + ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform() + + """ + if element: + self.click(element) + if self._driver.w3c: + self.w3c_actions.key_action.key_up(value) + self.w3c_actions.pointer_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, + {"value": keys_to_typing(value)})) + return self + + def move_by_offset(self, xoffset, yoffset): + """ + Moving the mouse to an offset from current mouse position. + + :Args: + - xoffset: X offset to move to, as a positive or negative integer. + - yoffset: Y offset to move to, as a positive or negative integer. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_by(xoffset, yoffset) + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.MOVE_TO, { + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def move_to_element(self, to_element): + """ + Moving the mouse to the middle of an element. + + :Args: + - to_element: The WebElement to move to. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_to(to_element) + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute( + Command.MOVE_TO, {'element': to_element.id})) + return self + + def move_to_element_with_offset(self, to_element, xoffset, yoffset): + """ + Move the mouse by an offset of the specified element. + Offsets are relative to the top-left corner of the element. + + :Args: + - to_element: The WebElement to move to. + - xoffset: X offset to move to. + - yoffset: Y offset to move to. + """ + if self._driver.w3c: + self.w3c_actions.pointer_action.move_to(to_element, xoffset, yoffset) + self.w3c_actions.key_action.pause() + else: + self._actions.append( + lambda: self._driver.execute(Command.MOVE_TO, { + 'element': to_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def pause(self, seconds): + """ Pause all inputs for the specified duration in seconds """ + if self._driver.w3c: + self.w3c_actions.pointer_action.pause(seconds) + self.w3c_actions.key_action.pause(seconds) + else: + self._actions.append(lambda: time.sleep(seconds)) + return self + + def release(self, on_element=None): + """ + Releasing a held mouse button on an element. + + :Args: + - on_element: The element to mouse up. + If None, releases on current mouse position. + """ + if on_element: + self.move_to_element(on_element) + if self._driver.w3c: + self.w3c_actions.pointer_action.release() + self.w3c_actions.key_action.pause() + else: + self._actions.append(lambda: self._driver.execute(Command.MOUSE_UP, {})) + return self + + def send_keys(self, *keys_to_send): + """ + Sends keys to current focused element. + + :Args: + - keys_to_send: The keys to send. Modifier keys constants can be found in the + 'Keys' class. + """ + typing = keys_to_typing(keys_to_send) + if self._driver.w3c: + for key in typing: + self.key_down(key) + self.key_up(key) + else: + self._actions.append(lambda: self._driver.execute( + Command.SEND_KEYS_TO_ACTIVE_ELEMENT, {'value': typing})) + return self + + def send_keys_to_element(self, element, *keys_to_send): + """ + Sends keys to an element. + + :Args: + - element: The element to send keys. + - keys_to_send: The keys to send. Modifier keys constants can be found in the + 'Keys' class. + """ + self.click(element) + self.send_keys(*keys_to_send) + return self + + # Context manager so ActionChains can be used in a 'with .. as' statements. + def __enter__(self): + return self # Return created instance of self. + + def __exit__(self, _type, _value, _traceback): + pass # Do nothing, does not require additional cleanup. diff --git a/selenium/webdriver/common/actions/__init__.py b/selenium/webdriver/common/actions/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/common/actions/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/common/actions/action_builder.py b/selenium/webdriver/common/actions/action_builder.py new file mode 100644 index 00000000..4b43ccc5 --- /dev/null +++ b/selenium/webdriver/common/actions/action_builder.py @@ -0,0 +1,85 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.command import Command +from . import interaction +from .key_actions import KeyActions +from .key_input import KeyInput +from .pointer_actions import PointerActions +from .pointer_input import PointerInput + + +class ActionBuilder(object): + def __init__(self, driver, mouse=None, keyboard=None): + if mouse is None: + mouse = PointerInput(interaction.POINTER_MOUSE, "mouse") + if keyboard is None: + keyboard = KeyInput(interaction.KEY) + self.devices = [mouse, keyboard] + self._key_action = KeyActions(keyboard) + self._pointer_action = PointerActions(mouse) + self.driver = driver + + def get_device_with(self, name): + try: + idx = self.devices.index(name) + return self.devices[idx] + except: + pass + + @property + def pointer_inputs(self): + return [device for device in self.devices if device.type == interaction.POINTER] + + @property + def key_inputs(self): + return [device for device in self.devices if device.type == interaction.KEY] + + @property + def key_action(self): + return self._key_action + + @property + def pointer_action(self): + return self._pointer_action + + def add_key_input(self, name): + new_input = KeyInput(name) + self._add_input(new_input) + return new_input + + def add_pointer_input(self, kind, name): + new_input = PointerInput(kind, name) + self._add_input(new_input) + return new_input + + def perform(self): + enc = {"actions": []} + for device in self.devices: + encoded = device.encode() + if encoded['actions']: + enc["actions"].append(encoded) + self.driver.execute(Command.W3C_ACTIONS, enc) + + def clear_actions(self): + """ + Clears actions that are already stored on the remote end + """ + self.driver.execute(Command.W3C_CLEAR_ACTIONS) + + def _add_input(self, input): + self.devices.append(input) diff --git a/selenium/webdriver/common/actions/input_device.py b/selenium/webdriver/common/actions/input_device.py new file mode 100644 index 00000000..984ef310 --- /dev/null +++ b/selenium/webdriver/common/actions/input_device.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import uuid + + +class InputDevice(object): + """ + Describes the input device being used for the action. + """ + def __init__(self, name=None): + if name is None: + self.name = uuid.uuid4() + else: + self.name = name + + self.actions = [] + + def add_action(self, action): + """ + + """ + self.actions.append(action) + + def clear_actions(self): + self.actions = [] + + def create_pause(self, duraton=0): + pass diff --git a/selenium/webdriver/common/actions/interaction.py b/selenium/webdriver/common/actions/interaction.py new file mode 100644 index 00000000..68047fc7 --- /dev/null +++ b/selenium/webdriver/common/actions/interaction.py @@ -0,0 +1,50 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +KEY = "key" +POINTER = "pointer" +NONE = "none" +SOURCE_TYPES = set([KEY, POINTER, NONE]) + +POINTER_MOUSE = "mouse" +POINTER_TOUCH = "touch" +POINTER_PEN = "pen" + +POINTER_KINDS = set([POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN]) + + +class Interaction(object): + + PAUSE = "pause" + + def __init__(self, source): + self.source = source + + +class Pause(Interaction): + + def __init__(self, source, duration=0): + super(Interaction, self).__init__() + self.source = source + self.duration = duration + + def encode(self): + return { + "type": self.PAUSE, + "duration": int(self.duration * 1000) + } diff --git a/selenium/webdriver/common/actions/key_actions.py b/selenium/webdriver/common/actions/key_actions.py new file mode 100644 index 00000000..c4d07299 --- /dev/null +++ b/selenium/webdriver/common/actions/key_actions.py @@ -0,0 +1,50 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from .interaction import Interaction, KEY +from .key_input import KeyInput +from ..utils import keys_to_typing + + +class KeyActions(Interaction): + + def __init__(self, source=None): + if source is None: + source = KeyInput(KEY) + self.source = source + super(KeyActions, self).__init__(source) + + def key_down(self, letter): + return self._key_action("create_key_down", letter) + + def key_up(self, letter): + return self._key_action("create_key_up", letter) + + def pause(self, duration=0): + return self._key_action("create_pause", duration) + + def send_keys(self, text): + if not isinstance(text, list): + text = keys_to_typing(text) + for letter in text: + self.key_down(letter) + self.key_up(letter) + return self + + def _key_action(self, action, letter): + meth = getattr(self.source, action) + meth(letter) + return self diff --git a/selenium/webdriver/common/actions/key_input.py b/selenium/webdriver/common/actions/key_input.py new file mode 100644 index 00000000..f392c6bc --- /dev/null +++ b/selenium/webdriver/common/actions/key_input.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from . import interaction + +from .input_device import InputDevice +from .interaction import (Interaction, + Pause) + + +class KeyInput(InputDevice): + def __init__(self, name): + super(KeyInput, self).__init__() + self.name = name + self.type = interaction.KEY + + def encode(self): + return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]} + + def create_key_down(self, key): + self.add_action(TypingInteraction(self, "keyDown", key)) + + def create_key_up(self, key): + self.add_action(TypingInteraction(self, "keyUp", key)) + + def create_pause(self, pause_duration=0): + self.add_action(Pause(self, pause_duration)) + + +class TypingInteraction(Interaction): + + def __init__(self, source, type_, key): + super(TypingInteraction, self).__init__(source) + self.type = type_ + self.key = key + + def encode(self): + return {"type": self.type, "value": self.key} diff --git a/selenium/webdriver/common/actions/mouse_button.py b/selenium/webdriver/common/actions/mouse_button.py new file mode 100644 index 00000000..5fe1b38b --- /dev/null +++ b/selenium/webdriver/common/actions/mouse_button.py @@ -0,0 +1,23 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +class MouseButton(object): + + LEFT = 0 + MIDDLE = 1 + RIGHT = 2 diff --git a/selenium/webdriver/common/actions/pointer_actions.py b/selenium/webdriver/common/actions/pointer_actions.py new file mode 100644 index 00000000..f9368574 --- /dev/null +++ b/selenium/webdriver/common/actions/pointer_actions.py @@ -0,0 +1,101 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from . import interaction + +from .interaction import Interaction +from .mouse_button import MouseButton +from .pointer_input import PointerInput + +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support.event_firing_webdriver import EventFiringWebElement + + +class PointerActions(Interaction): + + def __init__(self, source=None): + if source is None: + source = PointerInput(interaction.POINTER_MOUSE, "mouse") + self.source = source + super(PointerActions, self).__init__(source) + + def pointer_down(self, button=MouseButton.LEFT): + self._button_action("create_pointer_down", button=button) + + def pointer_up(self, button=MouseButton.LEFT): + self._button_action("create_pointer_up", button=button) + + def move_to(self, element, x=None, y=None): + if not isinstance(element, (WebElement, EventFiringWebElement)): + raise AttributeError("move_to requires a WebElement") + if x is not None or y is not None: + el_rect = element.rect + left_offset = el_rect['width'] / 2 + top_offset = el_rect['height'] / 2 + left = -left_offset + (x or 0) + top = -top_offset + (y or 0) + else: + left = 0 + top = 0 + self.source.create_pointer_move(origin=element, x=int(left), y=int(top)) + return self + + def move_by(self, x, y): + self.source.create_pointer_move(origin=interaction.POINTER, x=int(x), y=int(y)) + return self + + def move_to_location(self, x, y): + self.source.create_pointer_move(origin='viewport', x=int(x), y=int(y)) + return self + + def click(self, element=None): + if element: + self.move_to(element) + self.pointer_down(MouseButton.LEFT) + self.pointer_up(MouseButton.LEFT) + return self + + def context_click(self, element=None): + if element: + self.move_to(element) + self.pointer_down(MouseButton.RIGHT) + self.pointer_up(MouseButton.RIGHT) + return self + + def click_and_hold(self, element=None): + if element: + self.move_to(element) + self.pointer_down() + return self + + def release(self): + self.pointer_up() + return self + + def double_click(self, element=None): + if element: + self.move_to(element) + self.click() + self.click() + + def pause(self, duration=0): + self.source.create_pause(duration) + return self + + def _button_action(self, action, button=MouseButton.LEFT): + meth = getattr(self.source, action) + meth(button) + return self diff --git a/selenium/webdriver/common/actions/pointer_input.py b/selenium/webdriver/common/actions/pointer_input.py new file mode 100644 index 00000000..2216a5a2 --- /dev/null +++ b/selenium/webdriver/common/actions/pointer_input.py @@ -0,0 +1,64 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from .input_device import InputDevice +from .interaction import POINTER, POINTER_KINDS + +from selenium.common.exceptions import InvalidArgumentException +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support.event_firing_webdriver import EventFiringWebElement + + +class PointerInput(InputDevice): + + DEFAULT_MOVE_DURATION = 250 + + def __init__(self, kind, name): + super(PointerInput, self).__init__() + if (kind not in POINTER_KINDS): + raise InvalidArgumentException("Invalid PointerInput kind '%s'" % kind) + self.type = POINTER + self.kind = kind + self.name = name + + def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None): + action = dict(type="pointerMove", duration=duration) + action["x"] = x + action["y"] = y + if isinstance(origin, (WebElement, EventFiringWebElement)): + action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id} + elif origin is not None: + action["origin"] = origin + + self.add_action(action) + + def create_pointer_down(self, button): + self.add_action({"type": "pointerDown", "duration": 0, "button": button}) + + def create_pointer_up(self, button): + self.add_action({"type": "pointerUp", "duration": 0, "button": button}) + + def create_pointer_cancel(self): + self.add_action({"type": "pointerCancel"}) + + def create_pause(self, pause_duration): + self.add_action({"type": "pause", "duration": int(pause_duration * 1000)}) + + def encode(self): + return {"type": self.type, + "parameters": {"pointerType": self.kind}, + "id": self.name, + "actions": [acts for acts in self.actions]} diff --git a/selenium/webdriver/common/alert.py b/selenium/webdriver/common/alert.py new file mode 100644 index 00000000..65e97fc8 --- /dev/null +++ b/selenium/webdriver/common/alert.py @@ -0,0 +1,105 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Alert implementation. +""" + +from selenium.webdriver.common.utils import keys_to_typing +from selenium.webdriver.remote.command import Command + + +class Alert(object): + """ + Allows to work with alerts. + + Use this class to interact with alert prompts. It contains methods for dismissing, + accepting, inputting, and getting text from alert prompts. + + Accepting / Dismissing alert prompts:: + + Alert(driver).accept() + Alert(driver).dismiss() + + Inputting a value into an alert prompt: + + name_prompt = Alert(driver) + name_prompt.send_keys("Willian Shakesphere") + name_prompt.accept() + + + Reading a the text of a prompt for verification: + + alert_text = Alert(driver).text + self.assertEqual("Do you wish to quit?", alert_text) + + """ + + def __init__(self, driver): + """ + Creates a new Alert. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self.driver = driver + + @property + def text(self): + """ + Gets the text of the Alert. + """ + if self.driver.w3c: + return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"] + else: + return self.driver.execute(Command.GET_ALERT_TEXT)["value"] + + def dismiss(self): + """ + Dismisses the alert available. + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_DISMISS_ALERT) + else: + self.driver.execute(Command.DISMISS_ALERT) + + def accept(self): + """ + Accepts the alert available. + + Usage:: + Alert(driver).accept() # Confirm a alert dialog. + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_ACCEPT_ALERT) + else: + self.driver.execute(Command.ACCEPT_ALERT) + + def send_keys(self, keysToSend): + """ + Send Keys to the Alert. + + :Args: + - keysToSend: The text to be sent to Alert. + + + """ + if self.driver.w3c: + self.driver.execute(Command.W3C_SET_ALERT_VALUE, {'value': keys_to_typing(keysToSend), + 'text': keysToSend}) + else: + self.driver.execute(Command.SET_ALERT_VALUE, {'text': keysToSend}) diff --git a/selenium/webdriver/common/by.py b/selenium/webdriver/common/by.py new file mode 100644 index 00000000..7b1228d2 --- /dev/null +++ b/selenium/webdriver/common/by.py @@ -0,0 +1,35 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The By implementation. +""" + + +class By(object): + """ + Set of supported locator strategies. + """ + + ID = "id" + XPATH = "xpath" + LINK_TEXT = "link text" + PARTIAL_LINK_TEXT = "partial link text" + NAME = "name" + TAG_NAME = "tag name" + CLASS_NAME = "class name" + CSS_SELECTOR = "css selector" diff --git a/selenium/webdriver/common/desired_capabilities.py b/selenium/webdriver/common/desired_capabilities.py new file mode 100644 index 00000000..07c7cacc --- /dev/null +++ b/selenium/webdriver/common/desired_capabilities.py @@ -0,0 +1,127 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Desired Capabilities implementation. +""" + + +class DesiredCapabilities(object): + """ + Set of default supported desired capabilities. + + Use this as a starting point for creating a desired capabilities object for + requesting remote webdrivers for connecting to selenium server or selenium grid. + + Usage Example:: + + from selenium import webdriver + + selenium_grid_url = "http://198.0.0.1:4444/wd/hub" + + # Create a desired capabilities object as a starting point. + capabilities = DesiredCapabilities.FIREFOX.copy() + capabilities['platform'] = "WINDOWS" + capabilities['version'] = "10" + + # Instantiate an instance of Remote WebDriver with the desired capabilities. + driver = webdriver.Remote(desired_capabilities=capabilities, + command_executor=selenium_grid_url) + + Note: Always use '.copy()' on the DesiredCapabilities object to avoid the side + effects of altering the Global class instance. + + """ + + FIREFOX = { + "browserName": "firefox", + "acceptInsecureCerts": True, + } + + INTERNETEXPLORER = { + "browserName": "internet explorer", + "version": "", + "platform": "WINDOWS", + } + + EDGE = { + "browserName": "MicrosoftEdge", + "version": "", + "platform": "WINDOWS" + } + + CHROME = { + "browserName": "chrome", + "version": "", + "platform": "ANY", + } + + OPERA = { + "browserName": "opera", + "version": "", + "platform": "ANY", + } + + SAFARI = { + "browserName": "safari", + "version": "", + "platform": "MAC", + } + + HTMLUNIT = { + "browserName": "htmlunit", + "version": "", + "platform": "ANY", + } + + HTMLUNITWITHJS = { + "browserName": "htmlunit", + "version": "firefox", + "platform": "ANY", + "javascriptEnabled": True, + } + + IPHONE = { + "browserName": "iPhone", + "version": "", + "platform": "MAC", + } + + IPAD = { + "browserName": "iPad", + "version": "", + "platform": "MAC", + } + + ANDROID = { + "browserName": "android", + "version": "", + "platform": "ANDROID", + } + + PHANTOMJS = { + "browserName": "phantomjs", + "version": "", + "platform": "ANY", + "javascriptEnabled": True, + } + + WEBKITGTK = { + "browserName": "MiniBrowser", + "version": "", + "platform": "ANY", + } diff --git a/selenium/webdriver/common/html5/__init__.py b/selenium/webdriver/common/html5/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/common/html5/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/common/html5/application_cache.py b/selenium/webdriver/common/html5/application_cache.py new file mode 100644 index 00000000..ecfe5e10 --- /dev/null +++ b/selenium/webdriver/common/html5/application_cache.py @@ -0,0 +1,48 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The ApplicationCache implementaion. +""" + +from selenium.webdriver.remote.command import Command + + +class ApplicationCache(object): + + UNCACHED = 0 + IDLE = 1 + CHECKING = 2 + DOWNLOADING = 3 + UPDATE_READY = 4 + OBSOLETE = 5 + + def __init__(self, driver): + """ + Creates a new Aplication Cache. + + :Args: + - driver: The WebDriver instance which performs user actions. + """ + self.driver = driver + + @property + def status(self): + """ + Returns a current status of application cache. + """ + return self.driver.execute(Command.GET_APP_CACHE_STATUS)['value'] diff --git a/selenium/webdriver/common/keys.py b/selenium/webdriver/common/keys.py new file mode 100644 index 00000000..cd3bb76c --- /dev/null +++ b/selenium/webdriver/common/keys.py @@ -0,0 +1,96 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Keys implementation. +""" + +from __future__ import unicode_literals + + +class Keys(object): + """ + Set of special keys codes. + """ + + NULL = '\ue000' + CANCEL = '\ue001' # ^break + HELP = '\ue002' + BACKSPACE = '\ue003' + BACK_SPACE = BACKSPACE + TAB = '\ue004' + CLEAR = '\ue005' + RETURN = '\ue006' + ENTER = '\ue007' + SHIFT = '\ue008' + LEFT_SHIFT = SHIFT + CONTROL = '\ue009' + LEFT_CONTROL = CONTROL + ALT = '\ue00a' + LEFT_ALT = ALT + PAUSE = '\ue00b' + ESCAPE = '\ue00c' + SPACE = '\ue00d' + PAGE_UP = '\ue00e' + PAGE_DOWN = '\ue00f' + END = '\ue010' + HOME = '\ue011' + LEFT = '\ue012' + ARROW_LEFT = LEFT + UP = '\ue013' + ARROW_UP = UP + RIGHT = '\ue014' + ARROW_RIGHT = RIGHT + DOWN = '\ue015' + ARROW_DOWN = DOWN + INSERT = '\ue016' + DELETE = '\ue017' + SEMICOLON = '\ue018' + EQUALS = '\ue019' + + NUMPAD0 = '\ue01a' # number pad keys + NUMPAD1 = '\ue01b' + NUMPAD2 = '\ue01c' + NUMPAD3 = '\ue01d' + NUMPAD4 = '\ue01e' + NUMPAD5 = '\ue01f' + NUMPAD6 = '\ue020' + NUMPAD7 = '\ue021' + NUMPAD8 = '\ue022' + NUMPAD9 = '\ue023' + MULTIPLY = '\ue024' + ADD = '\ue025' + SEPARATOR = '\ue026' + SUBTRACT = '\ue027' + DECIMAL = '\ue028' + DIVIDE = '\ue029' + + F1 = '\ue031' # function keys + F2 = '\ue032' + F3 = '\ue033' + F4 = '\ue034' + F5 = '\ue035' + F6 = '\ue036' + F7 = '\ue037' + F8 = '\ue038' + F9 = '\ue039' + F10 = '\ue03a' + F11 = '\ue03b' + F12 = '\ue03c' + + META = '\ue03d' + COMMAND = '\ue03d' diff --git a/selenium/webdriver/common/options.py b/selenium/webdriver/common/options.py new file mode 100644 index 00000000..06f74b54 --- /dev/null +++ b/selenium/webdriver/common/options.py @@ -0,0 +1,74 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from abc import ABCMeta, abstractmethod + + +class BaseOptions(object): + """ + Base class for individual browser options + """ + __metaclass__ = ABCMeta + + def __init__(self): + self._caps = self.default_capabilities + + @property + def capabilities(self): + return self._caps + + def set_capability(self, name, value): + """ Sets a capability """ + self._caps[name] = value + + @abstractmethod + def to_capabilities(self): + return + + @property + @abstractmethod + def default_capabilities(self): + return {} + + +class ArgOptions(BaseOptions): + + def __init__(self): + super(ArgOptions, self).__init__() + self._arguments = [] + + @property + def arguments(self): + """ + :Returns: A list of arguments needed for the browser + """ + return self._arguments + + def add_argument(self, argument): + """ + Adds an argument to the list + + :Args: + - Sets the arguments + """ + if argument: + self._arguments.append(argument) + else: + raise ValueError('argument can not be null') + + def to_capabilities(self): + return self._caps diff --git a/selenium/webdriver/common/proxy.py b/selenium/webdriver/common/proxy.py new file mode 100644 index 00000000..2dd11a7b --- /dev/null +++ b/selenium/webdriver/common/proxy.py @@ -0,0 +1,358 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Proxy implementation. +""" + + +class ProxyTypeFactory: + """ + Factory for proxy types. + """ + + @staticmethod + def make(ff_value, string): + return {'ff_value': ff_value, 'string': string} + + +class ProxyType: + """ + Set of possible types of proxy. + + Each proxy type has 2 properties: + 'ff_value' is value of Firefox profile preference, + 'string' is id of proxy type. + """ + + DIRECT = ProxyTypeFactory.make(0, 'DIRECT') # Direct connection, no proxy (default on Windows). + MANUAL = ProxyTypeFactory.make(1, 'MANUAL') # Manual proxy settings (e.g., for httpProxy). + PAC = ProxyTypeFactory.make(2, 'PAC') # Proxy autoconfiguration from URL. + RESERVED_1 = ProxyTypeFactory.make(3, 'RESERVED1') # Never used. + AUTODETECT = ProxyTypeFactory.make(4, 'AUTODETECT') # Proxy autodetection (presumably with WPAD). + SYSTEM = ProxyTypeFactory.make(5, 'SYSTEM') # Use system settings (default on Linux). + UNSPECIFIED = ProxyTypeFactory.make(6, 'UNSPECIFIED') # Not initialized (for internal use). + + @classmethod + def load(cls, value): + if isinstance(value, dict) and 'string' in value: + value = value['string'] + value = str(value).upper() + for attr in dir(cls): + attr_value = getattr(cls, attr) + if isinstance(attr_value, dict) and \ + 'string' in attr_value and \ + attr_value['string'] is not None and \ + attr_value['string'] == value: + return attr_value + raise Exception("No proxy type is found for %s" % (value)) + + +class Proxy(object): + """ + Proxy contains information about proxy type and necessary proxy settings. + """ + + proxyType = ProxyType.UNSPECIFIED + autodetect = False + ftpProxy = '' + httpProxy = '' + noProxy = '' + proxyAutoconfigUrl = '' + sslProxy = '' + socksProxy = '' + socksUsername = '' + socksPassword = '' + socksVersion = None + + def __init__(self, raw=None): + """ + Creates a new Proxy. + + :Args: + - raw: raw proxy data. If None, default class values are used. + """ + if raw is not None: + if 'proxyType' in raw and raw['proxyType'] is not None: + self.proxy_type = ProxyType.load(raw['proxyType']) + if 'ftpProxy' in raw and raw['ftpProxy'] is not None: + self.ftp_proxy = raw['ftpProxy'] + if 'httpProxy' in raw and raw['httpProxy'] is not None: + self.http_proxy = raw['httpProxy'] + if 'noProxy' in raw and raw['noProxy'] is not None: + self.no_proxy = raw['noProxy'] + if 'proxyAutoconfigUrl' in raw and raw['proxyAutoconfigUrl'] is not None: + self.proxy_autoconfig_url = raw['proxyAutoconfigUrl'] + if 'sslProxy' in raw and raw['sslProxy'] is not None: + self.sslProxy = raw['sslProxy'] + if 'autodetect' in raw and raw['autodetect'] is not None: + self.auto_detect = raw['autodetect'] + if 'socksProxy' in raw and raw['socksProxy'] is not None: + self.socks_proxy = raw['socksProxy'] + if 'socksUsername' in raw and raw['socksUsername'] is not None: + self.socks_username = raw['socksUsername'] + if 'socksPassword' in raw and raw['socksPassword'] is not None: + self.socks_password = raw['socksPassword'] + if 'socksVersion' in raw and raw['socksVersion'] is not None: + self.socks_version = raw['socksVersion'] + + @property + def proxy_type(self): + """ + Returns proxy type as `ProxyType`. + """ + return self.proxyType + + @proxy_type.setter + def proxy_type(self, value): + """ + Sets proxy type. + + :Args: + - value: The proxy type. + """ + self._verify_proxy_type_compatibility(value) + self.proxyType = value + + @property + def auto_detect(self): + """ + Returns autodetect setting. + """ + return self.autodetect + + @auto_detect.setter + def auto_detect(self, value): + """ + Sets autodetect setting. + + :Args: + - value: The autodetect value. + """ + if isinstance(value, bool): + if self.autodetect is not value: + self._verify_proxy_type_compatibility(ProxyType.AUTODETECT) + self.proxyType = ProxyType.AUTODETECT + self.autodetect = value + else: + raise ValueError("Autodetect proxy value needs to be a boolean") + + @property + def ftp_proxy(self): + """ + Returns ftp proxy setting. + """ + return self.ftpProxy + + @ftp_proxy.setter + def ftp_proxy(self, value): + """ + Sets ftp proxy setting. + + :Args: + - value: The ftp proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.ftpProxy = value + + @property + def http_proxy(self): + """ + Returns http proxy setting. + """ + return self.httpProxy + + @http_proxy.setter + def http_proxy(self, value): + """ + Sets http proxy setting. + + :Args: + - value: The http proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.httpProxy = value + + @property + def no_proxy(self): + """ + Returns noproxy setting. + """ + return self.noProxy + + @no_proxy.setter + def no_proxy(self, value): + """ + Sets noproxy setting. + + :Args: + - value: The noproxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.noProxy = value + + @property + def proxy_autoconfig_url(self): + """ + Returns proxy autoconfig url setting. + """ + return self.proxyAutoconfigUrl + + @proxy_autoconfig_url.setter + def proxy_autoconfig_url(self, value): + """ + Sets proxy autoconfig url setting. + + :Args: + - value: The proxy autoconfig url value. + """ + self._verify_proxy_type_compatibility(ProxyType.PAC) + self.proxyType = ProxyType.PAC + self.proxyAutoconfigUrl = value + + @property + def ssl_proxy(self): + """ + Returns https proxy setting. + """ + return self.sslProxy + + @ssl_proxy.setter + def ssl_proxy(self, value): + """ + Sets https proxy setting. + + :Args: + - value: The https proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.sslProxy = value + + @property + def socks_proxy(self): + """ + Returns socks proxy setting. + """ + return self.socksProxy + + @socks_proxy.setter + def socks_proxy(self, value): + """ + Sets socks proxy setting. + + :Args: + - value: The socks proxy value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksProxy = value + + @property + def socks_username(self): + """ + Returns socks proxy username setting. + """ + return self.socksUsername + + @socks_username.setter + def socks_username(self, value): + """ + Sets socks proxy username setting. + + :Args: + - value: The socks proxy username value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksUsername = value + + @property + def socks_password(self): + """ + Returns socks proxy password setting. + """ + return self.socksPassword + + @socks_password.setter + def socks_password(self, value): + """ + Sets socks proxy password setting. + + :Args: + - value: The socks proxy password value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksPassword = value + + @property + def socks_version(self): + """ + Returns socks proxy version setting. + """ + return self.socksVersion + + @socks_version.setter + def socks_version(self, value): + """ + Sets socks proxy version setting. + + :Args: + - value: The socks proxy version value. + """ + self._verify_proxy_type_compatibility(ProxyType.MANUAL) + self.proxyType = ProxyType.MANUAL + self.socksVersion = value + + def _verify_proxy_type_compatibility(self, compatibleProxy): + if self.proxyType != ProxyType.UNSPECIFIED and self.proxyType != compatibleProxy: + raise Exception(" Specified proxy type (%s) not compatible with current setting (%s)" % (compatibleProxy, self.proxyType)) + + def add_to_capabilities(self, capabilities): + """ + Adds proxy information as capability in specified capabilities. + + :Args: + - capabilities: The capabilities to which proxy will be added. + """ + proxy_caps = {} + proxy_caps['proxyType'] = self.proxyType['string'] + if self.autodetect: + proxy_caps['autodetect'] = self.autodetect + if self.ftpProxy: + proxy_caps['ftpProxy'] = self.ftpProxy + if self.httpProxy: + proxy_caps['httpProxy'] = self.httpProxy + if self.proxyAutoconfigUrl: + proxy_caps['proxyAutoconfigUrl'] = self.proxyAutoconfigUrl + if self.sslProxy: + proxy_caps['sslProxy'] = self.sslProxy + if self.noProxy: + proxy_caps['noProxy'] = self.noProxy + if self.socksProxy: + proxy_caps['socksProxy'] = self.socksProxy + if self.socksUsername: + proxy_caps['socksUsername'] = self.socksUsername + if self.socksPassword: + proxy_caps['socksPassword'] = self.socksPassword + if self.socksVersion: + proxy_caps['socksVersion'] = self.socksVersion + capabilities['proxy'] = proxy_caps diff --git a/selenium/webdriver/common/service.py b/selenium/webdriver/common/service.py new file mode 100644 index 00000000..af04a9f2 --- /dev/null +++ b/selenium/webdriver/common/service.py @@ -0,0 +1,178 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import errno +import os +import platform +import subprocess +from subprocess import PIPE +import time +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common import utils + +try: + from subprocess import DEVNULL + _HAS_NATIVE_DEVNULL = True +except ImportError: + DEVNULL = -3 + _HAS_NATIVE_DEVNULL = False + + +class Service(object): + + def __init__(self, executable, port=0, log_file=DEVNULL, env=None, start_error_message=""): + self.path = executable + + self.port = port + if self.port == 0: + self.port = utils.free_port() + + if not _HAS_NATIVE_DEVNULL and log_file == DEVNULL: + log_file = open(os.devnull, 'wb') + + self.start_error_message = start_error_message + self.log_file = log_file + self.env = env or os.environ + + @property + def service_url(self): + """ + Gets the url of the Service + """ + return "http://%s" % utils.join_host_port('localhost', self.port) + + def command_line_args(self): + raise NotImplemented("This method needs to be implemented in a sub class") + + def start(self): + """ + Starts the Service. + + :Exceptions: + - WebDriverException : Raised either when it can't start the service + or when it can't connect to the service + """ + try: + cmd = [self.path] + cmd.extend(self.command_line_args()) + self.process = subprocess.Popen(cmd, env=self.env, + close_fds=platform.system() != 'Windows', + stdout=self.log_file, + stderr=self.log_file, + stdin=PIPE) + except TypeError: + raise + except OSError as err: + if err.errno == errno.ENOENT: + raise WebDriverException( + "'%s' executable needs to be in PATH. %s" % ( + os.path.basename(self.path), self.start_error_message) + ) + elif err.errno == errno.EACCES: + raise WebDriverException( + "'%s' executable may have wrong permissions. %s" % ( + os.path.basename(self.path), self.start_error_message) + ) + else: + raise + except Exception as e: + raise WebDriverException( + "The executable %s needs to be available in the path. %s\n%s" % + (os.path.basename(self.path), self.start_error_message, str(e))) + count = 0 + while True: + self.assert_process_still_running() + if self.is_connectable(): + break + count += 1 + time.sleep(1) + if count == 30: + raise WebDriverException("Can not connect to the Service %s" % self.path) + + def assert_process_still_running(self): + return_code = self.process.poll() + if return_code is not None: + raise WebDriverException( + 'Service %s unexpectedly exited. Status code was: %s' + % (self.path, return_code) + ) + + def is_connectable(self): + return utils.is_connectable(self.port) + + def send_remote_shutdown_command(self): + try: + from urllib import request as url_request + URLError = url_request.URLError + except ImportError: + import urllib2 as url_request + import urllib2 + URLError = urllib2.URLError + + try: + url_request.urlopen("%s/shutdown" % self.service_url) + except URLError: + return + + for x in range(30): + if not self.is_connectable(): + break + else: + time.sleep(1) + + def stop(self): + """ + Stops the service. + """ + if self.log_file != PIPE and not (self.log_file == DEVNULL and _HAS_NATIVE_DEVNULL): + try: + self.log_file.close() + except Exception: + pass + + if self.process is None: + return + + try: + self.send_remote_shutdown_command() + except TypeError: + pass + + try: + if self.process: + for stream in [self.process.stdin, + self.process.stdout, + self.process.stderr]: + try: + stream.close() + except AttributeError: + pass + self.process.terminate() + self.process.wait() + self.process.kill() + self.process = None + except OSError: + pass + + def __del__(self): + # `subprocess.Popen` doesn't send signal on `__del__`; + # so we attempt to close the launched process when `__del__` + # is triggered. + try: + self.stop() + except Exception: + pass diff --git a/selenium/webdriver/common/touch_actions.py b/selenium/webdriver/common/touch_actions.py new file mode 100644 index 00000000..89b25290 --- /dev/null +++ b/selenium/webdriver/common/touch_actions.py @@ -0,0 +1,192 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Touch Actions implementation +""" + +from selenium.webdriver.remote.command import Command + + +class TouchActions(object): + """ + Generate touch actions. Works like ActionChains; actions are stored in the + TouchActions object and are fired with perform(). + """ + + def __init__(self, driver): + """ + Creates a new TouchActions object. + + :Args: + - driver: The WebDriver instance which performs user actions. + It should be with touchscreen enabled. + """ + self._driver = driver + self._actions = [] + + def perform(self): + """ + Performs all stored actions. + """ + for action in self._actions: + action() + + def tap(self, on_element): + """ + Taps on a given element. + + :Args: + - on_element: The element to tap. + """ + self._actions.append(lambda: self._driver.execute( + Command.SINGLE_TAP, {'element': on_element.id})) + return self + + def double_tap(self, on_element): + """ + Double taps on a given element. + + :Args: + - on_element: The element to tap. + """ + self._actions.append(lambda: self._driver.execute( + Command.DOUBLE_TAP, {'element': on_element.id})) + return self + + def tap_and_hold(self, xcoord, ycoord): + """ + Touch down at given coordinates. + + :Args: + - xcoord: X Coordinate to touch down. + - ycoord: Y Coordinate to touch down. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_DOWN, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def move(self, xcoord, ycoord): + """ + Move held tap to specified location. + + :Args: + - xcoord: X Coordinate to move. + - ycoord: Y Coordinate to move. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_MOVE, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def release(self, xcoord, ycoord): + """ + Release previously issued tap 'and hold' command at specified location. + + :Args: + - xcoord: X Coordinate to release. + - ycoord: Y Coordinate to release. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_UP, { + 'x': int(xcoord), + 'y': int(ycoord)})) + return self + + def scroll(self, xoffset, yoffset): + """ + Touch and scroll, moving by xoffset and yoffset. + + :Args: + - xoffset: X offset to scroll to. + - yoffset: Y offset to scroll to. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_SCROLL, { + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def scroll_from_element(self, on_element, xoffset, yoffset): + """ + Touch and scroll starting at on_element, moving by xoffset and yoffset. + + :Args: + - on_element: The element where scroll starts. + - xoffset: X offset to scroll to. + - yoffset: Y offset to scroll to. + """ + self._actions.append(lambda: self._driver.execute( + Command.TOUCH_SCROLL, { + 'element': on_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset)})) + return self + + def long_press(self, on_element): + """ + Long press on an element. + + :Args: + - on_element: The element to long press. + """ + self._actions.append(lambda: self._driver.execute( + Command.LONG_PRESS, {'element': on_element.id})) + return self + + def flick(self, xspeed, yspeed): + """ + Flicks, starting anywhere on the screen. + + :Args: + - xspeed: The X speed in pixels per second. + - yspeed: The Y speed in pixels per second. + """ + self._actions.append(lambda: self._driver.execute( + Command.FLICK, { + 'xspeed': int(xspeed), + 'yspeed': int(yspeed)})) + return self + + def flick_element(self, on_element, xoffset, yoffset, speed): + """ + Flick starting at on_element, and moving by the xoffset and yoffset + with specified speed. + + :Args: + - on_element: Flick will start at center of element. + - xoffset: X offset to flick to. + - yoffset: Y offset to flick to. + - speed: Pixels per second to flick. + """ + self._actions.append(lambda: self._driver.execute( + Command.FLICK, { + 'element': on_element.id, + 'xoffset': int(xoffset), + 'yoffset': int(yoffset), + 'speed': int(speed)})) + return self + + # Context manager so TouchActions can be used in a 'with .. as' statements. + def __enter__(self): + return self # Return created instance of self. + + def __exit__(self, _type, _value, _traceback): + pass # Do nothing, does not require additional cleanup. diff --git a/selenium/webdriver/common/utils.py b/selenium/webdriver/common/utils.py new file mode 100644 index 00000000..957f2fd2 --- /dev/null +++ b/selenium/webdriver/common/utils.py @@ -0,0 +1,155 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Utils methods. +""" +import socket +from selenium.webdriver.common.keys import Keys + +try: + # Python 2 + basestring + _is_connectable_exceptions = (socket.error,) +except NameError: + # Python 3 + basestring = str + _is_connectable_exceptions = (socket.error, ConnectionResetError) + + +def free_port(): + """ + Determines a free port using sockets. + """ + free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + free_socket.bind(('0.0.0.0', 0)) + free_socket.listen(5) + port = free_socket.getsockname()[1] + free_socket.close() + return port + + +def find_connectable_ip(host, port=None): + """Resolve a hostname to an IP, preferring IPv4 addresses. + + We prefer IPv4 so that we don't change behavior from previous IPv4-only + implementations, and because some drivers (e.g., FirefoxDriver) do not + support IPv6 connections. + + If the optional port number is provided, only IPs that listen on the given + port are considered. + + :Args: + - host - A hostname. + - port - Optional port number. + + :Returns: + A single IP address, as a string. If any IPv4 address is found, one is + returned. Otherwise, if any IPv6 address is found, one is returned. If + neither, then None is returned. + + """ + try: + addrinfos = socket.getaddrinfo(host, None) + except socket.gaierror: + return None + + ip = None + for family, _, _, _, sockaddr in addrinfos: + connectable = True + if port: + connectable = is_connectable(port, sockaddr[0]) + + if connectable and family == socket.AF_INET: + return sockaddr[0] + if connectable and not ip and family == socket.AF_INET6: + ip = sockaddr[0] + return ip + + +def join_host_port(host, port): + """Joins a hostname and port together. + + This is a minimal implementation intended to cope with IPv6 literals. For + example, _join_host_port('::1', 80) == '[::1]:80'. + + :Args: + - host - A hostname. + - port - An integer port. + + """ + if ':' in host and not host.startswith('['): + return '[%s]:%d' % (host, port) + return '%s:%d' % (host, port) + + +def is_connectable(port, host="localhost"): + """ + Tries to connect to the server at port to see if it is running. + + :Args: + - port - The port to connect. + """ + socket_ = None + try: + socket_ = socket.create_connection((host, port), 1) + result = True + except _is_connectable_exceptions: + result = False + finally: + if socket_: + socket_.close() + return result + + +def is_url_connectable(port): + """ + Tries to connect to the HTTP server at /status path + and specified port to see if it responds successfully. + + :Args: + - port - The port to connect. + """ + try: + from urllib import request as url_request + except ImportError: + import urllib2 as url_request + + try: + res = url_request.urlopen("http://127.0.0.1:%s/status" % port) + if res.getcode() == 200: + return True + else: + return False + except Exception: + return False + + +def keys_to_typing(value): + """Processes the values that will be typed in the element.""" + typing = [] + for val in value: + if isinstance(val, Keys): + typing.append(val) + elif isinstance(val, int): + val = str(val) + for i in range(len(val)): + typing.append(val[i]) + else: + for i in range(len(val)): + typing.append(val[i]) + return typing diff --git a/selenium/webdriver/common/window.py b/selenium/webdriver/common/window.py new file mode 100644 index 00000000..73f5998c --- /dev/null +++ b/selenium/webdriver/common/window.py @@ -0,0 +1,29 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The WindowTypes implementation. +""" + +from __future__ import unicode_literals + + +class WindowTypes(object): + """Set of supported window types.""" + + TAB = 'tab' + WINDOW = 'window' diff --git a/selenium/webdriver/edge/__init__.py b/selenium/webdriver/edge/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/edge/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/edge/options.py b/selenium/webdriver/edge/options.py new file mode 100644 index 00000000..2fa5b038 --- /dev/null +++ b/selenium/webdriver/edge/options.py @@ -0,0 +1,51 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.options import BaseOptions + + +class Options(BaseOptions): + + def __init__(self): + super(Options, self).__init__() + self._page_load_strategy = "normal" + + @property + def page_load_strategy(self): + return self._page_load_strategy + + @page_load_strategy.setter + def page_load_strategy(self, value): + if value not in ['normal', 'eager', 'none']: + raise ValueError("Page Load Strategy should be 'normal', 'eager' or 'none'.") + self._page_load_strategy = value + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + + :Returns: A dictionary with everything + """ + caps = self._caps + caps['pageLoadStrategy'] = self._page_load_strategy + + return caps + + @property + def default_capabilities(self): + return DesiredCapabilities.EDGE.copy() diff --git a/selenium/webdriver/edge/service.py b/selenium/webdriver/edge/service.py new file mode 100644 index 00000000..4f2bf879 --- /dev/null +++ b/selenium/webdriver/edge/service.py @@ -0,0 +1,57 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common import service + + +class Service(service.Service): + + def __init__(self, executable_path, port=0, verbose=False, log_path=None): + """ + Creates a new instance of the EdgeDriver service. + + EdgeDriver provides an interface for Microsoft WebDriver to use + with Microsoft Edge. + + :param executable_path: Path to the Microsoft WebDriver binary. + :param port: Run the remote service on a specified port. + Defaults to 0, which binds to a random open port of the + system's choosing. + :verbose: Whether to make the webdriver more verbose (passes the + --verbose option to the binary). Defaults to False. + :param log_path: Optional path for the webdriver binary to log to. + Defaults to None which disables logging. + + """ + + self.service_args = [] + if verbose: + self.service_args.append("--verbose") + + params = { + "executable": executable_path, + "port": port, + "start_error_message": "Please download from https://go.microsoft.com/fwlink/?LinkId=619687" + } + + if log_path: + params["log_file"] = open(log_path, "a+") + + service.Service.__init__(self, **params) + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/selenium/webdriver/edge/webdriver.py b/selenium/webdriver/edge/webdriver.py new file mode 100644 index 00000000..22aae8e6 --- /dev/null +++ b/selenium/webdriver/edge/webdriver.py @@ -0,0 +1,92 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import warnings + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.edge.service import Service +from selenium.webdriver.remote.remote_connection import RemoteConnection +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver + +DEFAULT_PORT = 0 +DEFAULT_SERVICE_LOG_PATH = None + + +class WebDriver(RemoteWebDriver): + + def __init__(self, executable_path='MicrosoftWebDriver.exe', + capabilities=None, port=DEFAULT_PORT, verbose=False, + service_log_path=None, log_path=DEFAULT_SERVICE_LOG_PATH, + service=None, options=None, keep_alive=False): + """ + Creates a new instance of the chrome driver. + + Starts the service and then creates new instance of chrome driver. + + :Args: + - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH + - capabilities - Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - port - port you would like the service to run, if left as 0, a free port will be found. + - verbose - whether to set verbose logging in the service + - service_log_path - Where to log information from the driver. + - keep_alive - Whether to configure EdgeRemoteConnection to use HTTP keep-alive. + """ + if port != DEFAULT_PORT: + warnings.warn('port has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.port = port + + if service_log_path != DEFAULT_SERVICE_LOG_PATH: + warnings.warn('service_log_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if capabilities is not None: + warnings.warn('capabilities has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if service_log_path != DEFAULT_SERVICE_LOG_PATH: + warnings.warn('service_log_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if verbose: + warnings.warn('verbose has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + + if service: + self.service = service + else: + self.service = Service(executable_path, port=self.port, verbose=verbose, + log_path=service_log_path) + self.service.start() + + if capabilities is None: + capabilities = DesiredCapabilities.EDGE + + RemoteWebDriver.__init__( + self, + command_executor=RemoteConnection(self.service.service_url, + resolve_ip=False, + keep_alive=keep_alive), + desired_capabilities=capabilities) + self._is_remote = False + + @property + def edge_service(self): + warnings.warn("'edge_service' has been deprecated, please use 'service'", + DeprecationWarning, stacklevel=2) + return self.service + + def quit(self): + RemoteWebDriver.quit(self) + self.service.stop() diff --git a/selenium/webdriver/firefox/__init__.py b/selenium/webdriver/firefox/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/firefox/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/firefox/extension_connection.py b/selenium/webdriver/firefox/extension_connection.py new file mode 100644 index 00000000..ca715108 --- /dev/null +++ b/selenium/webdriver/firefox/extension_connection.py @@ -0,0 +1,84 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import logging +import time + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common import utils +from selenium.webdriver.remote.command import Command +from selenium.webdriver.remote.remote_connection import RemoteConnection +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary + +LOGGER = logging.getLogger(__name__) +PORT = 0 +HOST = None +_URL = "" + + +class ExtensionConnection(RemoteConnection): + def __init__(self, host, firefox_profile, firefox_binary=None, timeout=30): + self.profile = firefox_profile + self.binary = firefox_binary + HOST = host + timeout = int(timeout) + + if self.binary is None: + self.binary = FirefoxBinary() + + if HOST is None: + HOST = "127.0.0.1" + + PORT = utils.free_port() + self.profile.port = PORT + self.profile.update_preferences() + + self.profile.add_extension() + + self.binary.launch_browser(self.profile, timeout=timeout) + _URL = "http://%s:%d/hub" % (HOST, PORT) + RemoteConnection.__init__( + self, _URL, keep_alive=True) + + def quit(self, sessionId=None): + self.execute(Command.QUIT, {'sessionId': sessionId}) + while self.is_connectable(): + LOGGER.info("waiting to quit") + time.sleep(1) + + def connect(self): + """Connects to the extension and retrieves the session id.""" + return self.execute(Command.NEW_SESSION, + {'desiredCapabilities': DesiredCapabilities.FIREFOX}) + + @classmethod + def connect_and_quit(self): + """Connects to an running browser and quit immediately.""" + self._request('%s/extensions/firefox/quit' % _URL) + + @classmethod + def is_connectable(self): + """Trys to connect to the extension but do not retrieve context.""" + utils.is_connectable(self.profile.port) + + +class ExtensionConnectionError(Exception): + """An internal error occurred int the extension. + + Might be caused by bad input or bugs in webdriver + """ + pass diff --git a/selenium/webdriver/firefox/firefox_binary.py b/selenium/webdriver/firefox/firefox_binary.py new file mode 100644 index 00000000..f619f1e1 --- /dev/null +++ b/selenium/webdriver/firefox/firefox_binary.py @@ -0,0 +1,217 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +import os +import platform +from subprocess import Popen, STDOUT +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common import utils +import time + + +class FirefoxBinary(object): + + NO_FOCUS_LIBRARY_NAME = "x_ignore_nofocus.so" + + def __init__(self, firefox_path=None, log_file=None): + """ + Creates a new instance of Firefox binary. + + :Args: + - firefox_path - Path to the Firefox executable. By default, it will be detected from the standard locations. + - log_file - A file object to redirect the firefox process output to. It can be sys.stdout. + Please note that with parallel run the output won't be synchronous. + By default, it will be redirected to /dev/null. + """ + self._start_cmd = firefox_path + # We used to default to subprocess.PIPE instead of /dev/null, but after + # a while the pipe would fill up and Firefox would freeze. + self._log_file = log_file or open(os.devnull, "wb") + self.command_line = None + if self._start_cmd is None: + self._start_cmd = self._get_firefox_start_cmd() + if not self._start_cmd.strip(): + raise WebDriverException( + "Failed to find firefox binary. You can set it by specifying " + "the path to 'firefox_binary':\n\nfrom " + "selenium.webdriver.firefox.firefox_binary import " + "FirefoxBinary\n\nbinary = " + "FirefoxBinary('/path/to/binary')\ndriver = " + "webdriver.Firefox(firefox_binary=binary)") + # Rather than modifying the environment of the calling Python process + # copy it and modify as needed. + self._firefox_env = os.environ.copy() + self._firefox_env["MOZ_CRASHREPORTER_DISABLE"] = "1" + self._firefox_env["MOZ_NO_REMOTE"] = "1" + self._firefox_env["NO_EM_RESTART"] = "1" + + def add_command_line_options(self, *args): + self.command_line = args + + def launch_browser(self, profile, timeout=30): + """Launches the browser for the given profile name. + It is assumed the profile already exists. + """ + self.profile = profile + + self._start_from_profile_path(self.profile.path) + self._wait_until_connectable(timeout=timeout) + + def kill(self): + """Kill the browser. + + This is useful when the browser is stuck. + """ + if self.process: + self.process.kill() + self.process.wait() + + def _start_from_profile_path(self, path): + self._firefox_env["XRE_PROFILE_PATH"] = path + + if platform.system().lower() == 'linux': + self._modify_link_library_path() + command = [self._start_cmd, "-foreground"] + if self.command_line is not None: + for cli in self.command_line: + command.append(cli) + self.process = Popen( + command, stdout=self._log_file, stderr=STDOUT, + env=self._firefox_env) + + def _wait_until_connectable(self, timeout=30): + """Blocks until the extension is connectable in the firefox.""" + count = 0 + while not utils.is_connectable(self.profile.port): + if self.process.poll() is not None: + # Browser has exited + raise WebDriverException( + "The browser appears to have exited " + "before we could connect. If you specified a log_file in " + "the FirefoxBinary constructor, check it for details.") + if count >= timeout: + self.kill() + raise WebDriverException( + "Can't load the profile. Possible firefox version mismatch. " + "You must use GeckoDriver instead for Firefox 48+. Profile " + "Dir: %s If you specified a log_file in the " + "FirefoxBinary constructor, check it for details." + % (self.profile.path)) + count += 1 + time.sleep(1) + return True + + def _find_exe_in_registry(self): + try: + from _winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER + except ImportError: + from winreg import OpenKey, QueryValue, HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER + import shlex + keys = (r"SOFTWARE\Classes\FirefoxHTML\shell\open\command", + r"SOFTWARE\Classes\Applications\firefox.exe\shell\open\command") + command = "" + for path in keys: + try: + key = OpenKey(HKEY_LOCAL_MACHINE, path) + command = QueryValue(key, "") + break + except OSError: + try: + key = OpenKey(HKEY_CURRENT_USER, path) + command = QueryValue(key, "") + break + except OSError: + pass + else: + return "" + + if not command: + return "" + + return shlex.split(command)[0] + + def _get_firefox_start_cmd(self): + """Return the command to start firefox.""" + start_cmd = "" + if platform.system() == "Darwin": + start_cmd = "/Applications/Firefox.app/Contents/MacOS/firefox-bin" + # fallback to homebrew installation for mac users + if not os.path.exists(start_cmd): + start_cmd = os.path.expanduser("~") + start_cmd + elif platform.system() == "Windows": + start_cmd = (self._find_exe_in_registry() or self._default_windows_location()) + elif platform.system() == 'Java' and os._name == 'nt': + start_cmd = self._default_windows_location() + else: + for ffname in ["firefox", "iceweasel"]: + start_cmd = self.which(ffname) + if start_cmd is not None: + break + else: + # couldn't find firefox on the system path + raise RuntimeError( + "Could not find firefox in your system PATH." + + " Please specify the firefox binary location or install firefox") + return start_cmd + + def _default_windows_location(self): + program_files = [os.getenv("PROGRAMFILES", r"C:\Program Files"), + os.getenv("PROGRAMFILES(X86)", r"C:\Program Files (x86)")] + for path in program_files: + binary_path = os.path.join(path, r"Mozilla Firefox\firefox.exe") + if os.access(binary_path, os.X_OK): + return binary_path + return "" + + def _modify_link_library_path(self): + existing_ld_lib_path = os.environ.get('LD_LIBRARY_PATH', '') + + new_ld_lib_path = self._extract_and_check( + self.profile, self.NO_FOCUS_LIBRARY_NAME, "x86", "amd64") + + new_ld_lib_path += existing_ld_lib_path + + self._firefox_env["LD_LIBRARY_PATH"] = new_ld_lib_path + self._firefox_env['LD_PRELOAD'] = self.NO_FOCUS_LIBRARY_NAME + + def _extract_and_check(self, profile, no_focus_so_name, x86, amd64): + + paths = [x86, amd64] + built_path = "" + for path in paths: + library_path = os.path.join(profile.path, path) + if not os.path.exists(library_path): + os.makedirs(library_path) + import shutil + shutil.copy(os.path.join( + os.path.dirname(__file__), + path, + self.NO_FOCUS_LIBRARY_NAME), + library_path) + built_path += library_path + ":" + + return built_path + + def which(self, fname): + """Returns the fully qualified path by searching Path of the given + name""" + for pe in os.environ['PATH'].split(os.pathsep): + checkname = os.path.join(pe, fname) + if os.access(checkname, os.X_OK) and not os.path.isdir(checkname): + return checkname + return None diff --git a/selenium/webdriver/firefox/firefox_profile.py b/selenium/webdriver/firefox/firefox_profile.py new file mode 100644 index 00000000..f5b529ed --- /dev/null +++ b/selenium/webdriver/firefox/firefox_profile.py @@ -0,0 +1,364 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import with_statement + +import base64 +import copy +import json +import os +import re +import shutil +import sys +import tempfile +import zipfile + +try: + from cStringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO + +from xml.dom import minidom +from selenium.common.exceptions import WebDriverException + + +WEBDRIVER_EXT = "webdriver.xpi" +WEBDRIVER_PREFERENCES = "webdriver_prefs.json" +EXTENSION_NAME = "fxdriver@googlecode.com" + + +class AddonFormatError(Exception): + """Exception for not well-formed add-on manifest files""" + + +class FirefoxProfile(object): + ANONYMOUS_PROFILE_NAME = "WEBDRIVER_ANONYMOUS_PROFILE" + DEFAULT_PREFERENCES = None + + def __init__(self, profile_directory=None): + """ + Initialises a new instance of a Firefox Profile + + :args: + - profile_directory: Directory of profile that you want to use. If a + directory is passed in it will be cloned and the cloned directory + will be used by the driver when instantiated. + This defaults to None and will create a new + directory when object is created. + """ + if not FirefoxProfile.DEFAULT_PREFERENCES: + with open(os.path.join(os.path.dirname(__file__), + WEBDRIVER_PREFERENCES)) as default_prefs: + FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs) + + self.default_preferences = copy.deepcopy( + FirefoxProfile.DEFAULT_PREFERENCES['mutable']) + self.profile_dir = profile_directory + self.tempfolder = None + if self.profile_dir is None: + self.profile_dir = self._create_tempfolder() + else: + self.tempfolder = tempfile.mkdtemp() + newprof = os.path.join(self.tempfolder, "webdriver-py-profilecopy") + shutil.copytree(self.profile_dir, newprof, + ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock")) + self.profile_dir = newprof + os.chmod(self.profile_dir, 0o755) + self._read_existing_userjs(os.path.join(self.profile_dir, "user.js")) + self.extensionsDir = os.path.join(self.profile_dir, "extensions") + self.userPrefs = os.path.join(self.profile_dir, "user.js") + if os.path.isfile(self.userPrefs): + os.chmod(self.userPrefs, 0o644) + + # Public Methods + def set_preference(self, key, value): + """ + sets the preference that we want in the profile. + """ + self.default_preferences[key] = value + + def add_extension(self, extension=WEBDRIVER_EXT): + self._install_extension(extension) + + def update_preferences(self): + for key, value in FirefoxProfile.DEFAULT_PREFERENCES['frozen'].items(): + self.default_preferences[key] = value + self._write_user_prefs(self.default_preferences) + + # Properties + + @property + def path(self): + """ + Gets the profile directory that is currently being used + """ + return self.profile_dir + + @property + def port(self): + """ + Gets the port that WebDriver is working on + """ + return self._port + + @port.setter + def port(self, port): + """ + Sets the port that WebDriver will be running on + """ + if not isinstance(port, int): + raise WebDriverException("Port needs to be an integer") + try: + port = int(port) + if port < 1 or port > 65535: + raise WebDriverException("Port number must be in the range 1..65535") + except (ValueError, TypeError): + raise WebDriverException("Port needs to be an integer") + self._port = port + self.set_preference("webdriver_firefox_port", self._port) + + @property + def accept_untrusted_certs(self): + return self.default_preferences["webdriver_accept_untrusted_certs"] + + @accept_untrusted_certs.setter + def accept_untrusted_certs(self, value): + if value not in [True, False]: + raise WebDriverException("Please pass in a Boolean to this call") + self.set_preference("webdriver_accept_untrusted_certs", value) + + @property + def assume_untrusted_cert_issuer(self): + return self.default_preferences["webdriver_assume_untrusted_issuer"] + + @assume_untrusted_cert_issuer.setter + def assume_untrusted_cert_issuer(self, value): + if value not in [True, False]: + raise WebDriverException("Please pass in a Boolean to this call") + + self.set_preference("webdriver_assume_untrusted_issuer", value) + + @property + def encoded(self): + """ + A zipped, base64 encoded string of profile directory + for use with remote WebDriver JSON wire protocol + """ + self.update_preferences() + fp = BytesIO() + zipped = zipfile.ZipFile(fp, 'w', zipfile.ZIP_DEFLATED) + path_root = len(self.path) + 1 # account for trailing slash + for base, dirs, files in os.walk(self.path): + for fyle in files: + filename = os.path.join(base, fyle) + zipped.write(filename, filename[path_root:]) + zipped.close() + return base64.b64encode(fp.getvalue()).decode('UTF-8') + + def _create_tempfolder(self): + """ + Creates a temp folder to store User.js and the extension + """ + return tempfile.mkdtemp() + + def _write_user_prefs(self, user_prefs): + """ + writes the current user prefs dictionary to disk + """ + with open(self.userPrefs, "w") as f: + for key, value in user_prefs.items(): + f.write('user_pref("%s", %s);\n' % (key, json.dumps(value))) + + def _read_existing_userjs(self, userjs): + import warnings + + PREF_RE = re.compile(r'user_pref\("(.*)",\s(.*)\)') + try: + with open(userjs) as f: + for usr in f: + matches = re.search(PREF_RE, usr) + try: + self.default_preferences[matches.group(1)] = json.loads(matches.group(2)) + except Exception: + warnings.warn("(skipping) failed to json.loads existing preference: " + + matches.group(1) + matches.group(2)) + except Exception: + # The profile given hasn't had any changes made, i.e no users.js + pass + + def _install_extension(self, addon, unpack=True): + """ + Installs addon from a filepath, url + or directory of addons in the profile. + - path: url, absolute path to .xpi, or directory of addons + - unpack: whether to unpack unless specified otherwise in the install.rdf + """ + if addon == WEBDRIVER_EXT: + addon = os.path.join(os.path.dirname(__file__), WEBDRIVER_EXT) + + tmpdir = None + xpifile = None + if addon.endswith('.xpi'): + tmpdir = tempfile.mkdtemp(suffix='.' + os.path.split(addon)[-1]) + compressed_file = zipfile.ZipFile(addon, 'r') + for name in compressed_file.namelist(): + if name.endswith('/'): + if not os.path.isdir(os.path.join(tmpdir, name)): + os.makedirs(os.path.join(tmpdir, name)) + else: + if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))): + os.makedirs(os.path.dirname(os.path.join(tmpdir, name))) + data = compressed_file.read(name) + with open(os.path.join(tmpdir, name), 'wb') as f: + f.write(data) + xpifile = addon + addon = tmpdir + + # determine the addon id + addon_details = self._addon_details(addon) + addon_id = addon_details.get('id') + assert addon_id, 'The addon id could not be found: %s' % addon + + # copy the addon to the profile + addon_path = os.path.join(self.extensionsDir, addon_id) + if not unpack and not addon_details['unpack'] and xpifile: + if not os.path.exists(self.extensionsDir): + os.makedirs(self.extensionsDir) + os.chmod(self.extensionsDir, 0o755) + shutil.copy(xpifile, addon_path + '.xpi') + else: + if not os.path.exists(addon_path): + shutil.copytree(addon, addon_path, symlinks=True) + + # remove the temporary directory, if any + if tmpdir: + shutil.rmtree(tmpdir) + + def _addon_details(self, addon_path): + """ + Returns a dictionary of details about the addon. + + :param addon_path: path to the add-on directory or XPI + + Returns:: + + {'id': u'rainbow@colors.org', # id of the addon + 'version': u'1.4', # version of the addon + 'name': u'Rainbow', # name of the addon + 'unpack': False } # whether to unpack the addon + """ + + details = { + 'id': None, + 'unpack': False, + 'name': None, + 'version': None + } + + def get_namespace_id(doc, url): + attributes = doc.documentElement.attributes + namespace = "" + for i in range(attributes.length): + if attributes.item(i).value == url: + if ":" in attributes.item(i).name: + # If the namespace is not the default one remove 'xlmns:' + namespace = attributes.item(i).name.split(':')[1] + ":" + break + return namespace + + def get_text(element): + """Retrieve the text value of a given node""" + rc = [] + for node in element.childNodes: + if node.nodeType == node.TEXT_NODE: + rc.append(node.data) + return ''.join(rc).strip() + + def parse_manifest_json(content): + """Extracts the details from the contents of a WebExtensions `manifest.json` file.""" + manifest = json.loads(content) + try: + id = manifest['applications']['gecko']['id'] + except KeyError: + id = manifest['name'].replace(" ", "") + "@" + manifest['version'] + return { + 'id': id, + 'version': manifest['version'], + 'name': manifest['version'], + 'unpack': False, + } + + if not os.path.exists(addon_path): + raise IOError('Add-on path does not exist: %s' % addon_path) + + try: + if zipfile.is_zipfile(addon_path): + # Bug 944361 - We cannot use 'with' together with zipFile because + # it will cause an exception thrown in Python 2.6. + try: + compressed_file = zipfile.ZipFile(addon_path, 'r') + if 'manifest.json' in compressed_file.namelist(): + return parse_manifest_json(compressed_file.read('manifest.json')) + + manifest = compressed_file.read('install.rdf') + finally: + compressed_file.close() + elif os.path.isdir(addon_path): + manifest_json_filename = os.path.join(addon_path, 'manifest.json') + if os.path.exists(manifest_json_filename): + with open(manifest_json_filename, 'r') as f: + return parse_manifest_json(f.read()) + + with open(os.path.join(addon_path, 'install.rdf'), 'r') as f: + manifest = f.read() + else: + raise IOError('Add-on path is neither an XPI nor a directory: %s' % addon_path) + except (IOError, KeyError) as e: + raise AddonFormatError(str(e), sys.exc_info()[2]) + + try: + doc = minidom.parseString(manifest) + + # Get the namespaces abbreviations + em = get_namespace_id(doc, 'http://www.mozilla.org/2004/em-rdf#') + rdf = get_namespace_id(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#') + + description = doc.getElementsByTagName(rdf + 'Description').item(0) + if description is None: + description = doc.getElementsByTagName('Description').item(0) + for node in description.childNodes: + # Remove the namespace prefix from the tag for comparison + entry = node.nodeName.replace(em, "") + if entry in details.keys(): + details.update({entry: get_text(node)}) + if details.get('id') is None: + for i in range(description.attributes.length): + attribute = description.attributes.item(i) + if attribute.name == em + 'id': + details.update({'id': attribute.value}) + except Exception as e: + raise AddonFormatError(str(e), sys.exc_info()[2]) + + # turn unpack into a true/false value + if isinstance(details['unpack'], str): + details['unpack'] = details['unpack'].lower() == 'true' + + # If no ID is set, the add-on is invalid + if details.get('id') is None: + raise AddonFormatError('Add-on id could not be found.') + + return details diff --git a/selenium/webdriver/firefox/options.py b/selenium/webdriver/firefox/options.py new file mode 100644 index 00000000..913dcfe1 --- /dev/null +++ b/selenium/webdriver/firefox/options.py @@ -0,0 +1,171 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from selenium.common.exceptions import InvalidArgumentException +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.proxy import Proxy +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary +from selenium.webdriver.firefox.firefox_profile import FirefoxProfile +from selenium.webdriver.common.options import ArgOptions + + +class Log(object): + def __init__(self): + self.level = None + + def to_capabilities(self): + if self.level is not None: + return {"log": {"level": self.level}} + return {} + + +class Options(ArgOptions): + KEY = "moz:firefoxOptions" + + def __init__(self): + super(Options, self).__init__() + self._binary = None + self._preferences = {} + self._profile = None + self._proxy = None + self.log = Log() + + @property + def binary(self): + """Returns the FirefoxBinary instance""" + return self._binary + + @binary.setter + def binary(self, new_binary): + """Sets location of the browser binary, either by string or + ``FirefoxBinary`` instance. + + """ + if not isinstance(new_binary, FirefoxBinary): + new_binary = FirefoxBinary(new_binary) + self._binary = new_binary + + @property + def binary_location(self): + """ + :Returns: The location of the binary. + """ + return self.binary._start_cmd + + @binary_location.setter # noqa + def binary_location(self, value): + """ Sets the location of the browser binary by string """ + self.binary = value + + @property + def accept_insecure_certs(self): + return self._caps.get('acceptInsecureCerts') + + @accept_insecure_certs.setter + def accept_insecure_certs(self, value): + self._caps['acceptInsecureCerts'] = value + + @property + def preferences(self): + """:Returns: A dict of preferences.""" + return self._preferences + + def set_preference(self, name, value): + """Sets a preference.""" + self._preferences[name] = value + + @property + def proxy(self): + """ + :Returns: Proxy if set, otherwise None. + """ + return self._proxy + + @proxy.setter + def proxy(self, value): + if not isinstance(value, Proxy): + raise InvalidArgumentException("Only Proxy objects can be passed in.") + self._proxy = value + + @property + def profile(self): + """ + :Returns: The Firefox profile to use. + """ + return self._profile + + @profile.setter + def profile(self, new_profile): + """Sets location of the browser profile to use, either by string + or ``FirefoxProfile``. + + """ + if not isinstance(new_profile, FirefoxProfile): + new_profile = FirefoxProfile(new_profile) + self._profile = new_profile + + @property + def headless(self): + """ + :Returns: True if the headless argument is set, else False + """ + return '-headless' in self._arguments + + @headless.setter + def headless(self, value): + """ + Sets the headless argument + + Args: + value: boolean value indicating to set the headless option + """ + if value is True: + self._arguments.append('-headless') + elif '-headless' in self._arguments: + self._arguments.remove('-headless') + + def to_capabilities(self): + """Marshals the Firefox options to a `moz:firefoxOptions` + object. + """ + # This intentionally looks at the internal properties + # so if a binary or profile has _not_ been set, + # it will defer to geckodriver to find the system Firefox + # and generate a fresh profile. + caps = self._caps + opts = {} + + if self._binary is not None: + opts["binary"] = self._binary._start_cmd + if len(self._preferences) > 0: + opts["prefs"] = self._preferences + if self._proxy is not None: + self._proxy.add_to_capabilities(caps) + if self._profile is not None: + opts["profile"] = self._profile.encoded + if len(self._arguments) > 0: + opts["args"] = self._arguments + + opts.update(self.log.to_capabilities()) + + if len(opts) > 0: + caps[Options.KEY] = opts + + return caps + + @property + def default_capabilities(self): + return DesiredCapabilities.FIREFOX.copy() diff --git a/selenium/webdriver/firefox/remote_connection.py b/selenium/webdriver/firefox/remote_connection.py new file mode 100644 index 00000000..5b350008 --- /dev/null +++ b/selenium/webdriver/firefox/remote_connection.py @@ -0,0 +1,36 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.remote_connection import RemoteConnection + + +class FirefoxRemoteConnection(RemoteConnection): + def __init__(self, remote_server_addr, keep_alive=True): + RemoteConnection.__init__(self, remote_server_addr, keep_alive) + + self._commands["GET_CONTEXT"] = ('GET', '/session/$sessionId/moz/context') + self._commands["SET_CONTEXT"] = ("POST", "/session/$sessionId/moz/context") + self._commands["ELEMENT_GET_ANONYMOUS_CHILDREN"] = \ + ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_children") + self._commands["ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE"] = \ + ("POST", "/session/$sessionId/moz/xbl/$id/anonymous_by_attribute") + self._commands["INSTALL_ADDON"] = \ + ("POST", "/session/$sessionId/moz/addon/install") + self._commands["UNINSTALL_ADDON"] = \ + ("POST", "/session/$sessionId/moz/addon/uninstall") + self._commands["FULL_PAGE_SCREENSHOT"] = \ + ("GET", "/session/$sessionId/moz/screenshot/full") diff --git a/selenium/webdriver/firefox/service.py b/selenium/webdriver/firefox/service.py new file mode 100644 index 00000000..f762eb7c --- /dev/null +++ b/selenium/webdriver/firefox/service.py @@ -0,0 +1,54 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common import service + + +class Service(service.Service): + """Object that manages the starting and stopping of the + GeckoDriver.""" + + def __init__(self, executable_path, port=0, service_args=None, + log_path="geckodriver.log", env=None): + """Creates a new instance of the GeckoDriver remote service proxy. + + GeckoDriver provides a HTTP interface speaking the W3C WebDriver + protocol to Marionette. + + :param executable_path: Path to the GeckoDriver binary. + :param port: Run the remote service on a specified port. + Defaults to 0, which binds to a random open port of the + system's choosing. + :param service_args: Optional list of arguments to pass to the + GeckoDriver binary. + :param log_path: Optional path for the GeckoDriver to log to. + Defaults to _geckodriver.log_ in the current working directory. + :param env: Optional dictionary of output variables to expose + in the services' environment. + + """ + log_file = open(log_path, "a+") if log_path is not None and log_path != "" else None + + service.Service.__init__( + self, executable_path, port=port, log_file=log_file, env=env) + self.service_args = service_args or [] + + def command_line_args(self): + return ["--port", "%d" % self.port] + self.service_args + + def send_remote_shutdown_command(self): + pass diff --git a/selenium/webdriver/firefox/webdriver.py b/selenium/webdriver/firefox/webdriver.py new file mode 100644 index 00000000..119ba817 --- /dev/null +++ b/selenium/webdriver/firefox/webdriver.py @@ -0,0 +1,328 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +try: + basestring +except NameError: # Python 3.x + basestring = str + +import base64 +import shutil +import warnings +from contextlib import contextmanager + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver + +from .firefox_binary import FirefoxBinary +from .firefox_profile import FirefoxProfile +from .options import Options +from .remote_connection import FirefoxRemoteConnection +from .service import Service +from .webelement import FirefoxWebElement + + +DEFAULT_SERVICE_LOG_PATH = None + + +class WebDriver(RemoteWebDriver): + + CONTEXT_CHROME = "chrome" + CONTEXT_CONTENT = "content" + + _web_element_cls = FirefoxWebElement + + def __init__(self, firefox_profile=None, firefox_binary=None, + timeout=30, capabilities=None, proxy=None, + executable_path="geckodriver", options=None, + service_log_path="geckodriver.log", firefox_options=None, + service_args=None, service=None, desired_capabilities=None, log_path=None, + keep_alive=True): + """Starts a new local session of Firefox. + + Based on the combination and specificity of the various keyword + arguments, a capabilities dictionary will be constructed that + is passed to the remote end. + + The keyword arguments given to this constructor are helpers to + more easily allow Firefox WebDriver sessions to be customised + with different options. They are mapped on to a capabilities + dictionary that is passed on to the remote end. + + As some of the options, such as `firefox_profile` and + `options.profile` are mutually exclusive, precedence is + given from how specific the setting is. `capabilities` is the + least specific keyword argument, followed by `options`, + followed by `firefox_binary` and `firefox_profile`. + + In practice this means that if `firefox_profile` and + `options.profile` are both set, the selected profile + instance will always come from the most specific variable. + In this case that would be `firefox_profile`. This will result in + `options.profile` to be ignored because it is considered + a less specific setting than the top-level `firefox_profile` + keyword argument. Similarly, if you had specified a + `capabilities["moz:firefoxOptions"]["profile"]` Base64 string, + this would rank below `options.profile`. + + :param firefox_profile: Instance of ``FirefoxProfile`` object + or a string. If undefined, a fresh profile will be created + in a temporary location on the system. + :param firefox_binary: Instance of ``FirefoxBinary`` or full + path to the Firefox binary. If undefined, the system default + Firefox installation will be used. + :param timeout: Time to wait for Firefox to launch when using + the extension connection. + :param capabilities: Dictionary of desired capabilities. + :param proxy: The proxy settings to use when communicating with + Firefox via the extension connection. + :param executable_path: Full path to override which geckodriver + binary to use for Firefox 47.0.1 and greater, which + defaults to picking up the binary from the system path. + :param options: Instance of ``options.Options``. + :param service_log_path: Where to log information from the driver. + :param service_args: List of args to pass to the driver service + :param desired_capabilities: alias of capabilities. In future + versions of this library, this will replace 'capabilities'. + This will make the signature consistent with RemoteWebDriver. + :param keep_alive: Whether to configure remote_connection.RemoteConnection to use + HTTP keep-alive. + """ + + if executable_path != 'geckodriver': + warnings.warn('executable_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if capabilities is not None: + warnings.warn('capabilities has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if firefox_binary is not None: + warnings.warn('firefox_binary has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.binary = None + if firefox_profile is not None: + warnings.warn('firefox_profile has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.profile = None + + if log_path != DEFAULT_SERVICE_LOG_PATH: + warnings.warn('log_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + + self.service = service + + # If desired capabilities is set, alias it to capabilities. + # If both are set ignore desired capabilities. + if capabilities is None and desired_capabilities: + capabilities = desired_capabilities + + if capabilities is None: + capabilities = DesiredCapabilities.FIREFOX.copy() + if options is None: + options = Options() + + capabilities = dict(capabilities) + + if capabilities.get("binary"): + self.binary = capabilities["binary"] + + # options overrides capabilities + if options is not None: + if options.binary is not None: + self.binary = options.binary + if options.profile is not None: + self.profile = options.profile + + # firefox_binary and firefox_profile + # override options + if firefox_binary is not None: + if isinstance(firefox_binary, basestring): + firefox_binary = FirefoxBinary(firefox_binary) + self.binary = firefox_binary + options.binary = firefox_binary + if firefox_profile is not None: + if isinstance(firefox_profile, basestring): + firefox_profile = FirefoxProfile(firefox_profile) + self.profile = firefox_profile + options.profile = firefox_profile + + if self.service is None: + self.service = Service( + executable_path, + service_args=service_args, + log_path=service_log_path) + self.service.start() + + capabilities.update(options.to_capabilities()) + + executor = FirefoxRemoteConnection( + remote_server_addr=self.service.service_url) + RemoteWebDriver.__init__( + self, + command_executor=executor, + desired_capabilities=capabilities, + keep_alive=True) + + self._is_remote = False + + def quit(self): + """Quits the driver and close every associated window.""" + try: + RemoteWebDriver.quit(self) + except Exception: + # We don't care about the message because something probably has gone wrong + pass + + if self.w3c: + self.service.stop() + else: + self.binary.kill() + + if self.profile is not None: + try: + shutil.rmtree(self.profile.path) + if self.profile.tempfolder is not None: + shutil.rmtree(self.profile.tempfolder) + except Exception as e: + print(str(e)) + + @property + def firefox_profile(self): + return self.profile + + # Extension commands: + + def set_context(self, context): + self.execute("SET_CONTEXT", {"context": context}) + + @contextmanager + def context(self, context): + """Sets the context that Selenium commands are running in using + a `with` statement. The state of the context on the server is + saved before entering the block, and restored upon exiting it. + + :param context: Context, may be one of the class properties + `CONTEXT_CHROME` or `CONTEXT_CONTENT`. + + Usage example:: + + with selenium.context(selenium.CONTEXT_CHROME): + # chrome scope + ... do stuff ... + """ + initial_context = self.execute('GET_CONTEXT').pop('value') + self.set_context(context) + try: + yield + finally: + self.set_context(initial_context) + + def install_addon(self, path, temporary=None): + """ + Installs Firefox addon. + + Returns identifier of installed addon. This identifier can later + be used to uninstall addon. + + :param path: Absolute path to the addon that will be installed. + + :Usage: + :: + + driver.install_addon('/path/to/firebug.xpi') + """ + payload = {"path": path} + if temporary is not None: + payload["temporary"] = temporary + return self.execute("INSTALL_ADDON", payload)["value"] + + def uninstall_addon(self, identifier): + """ + Uninstalls Firefox addon using its identifier. + + :Usage: + :: + + driver.uninstall_addon('addon@foo.com') + """ + self.execute("UNINSTALL_ADDON", {"id": identifier}) + + def get_full_page_screenshot_as_file(self, filename): + """ + Saves a full document screenshot of the current window to a PNG image file. Returns + False if there is any IOError, else returns True. Use full paths in + your filename. + + :Args: + - filename: The full path you wish to save your screenshot to. This + should end with a `.png` extension. + + :Usage: + :: + + driver.get_full_page_screenshot_as_file('/Screenshots/foo.png') + """ + if not filename.lower().endswith('.png'): + warnings.warn("name used for saved screenshot does not match file " + "type. It should end with a `.png` extension", UserWarning) + png = self.get_full_page_screenshot_as_png() + try: + with open(filename, 'wb') as f: + f.write(png) + except IOError: + return False + finally: + del png + return True + + def save_full_page_screenshot(self, filename): + """ + Saves a full document screenshot of the current window to a PNG image file. Returns + False if there is any IOError, else returns True. Use full paths in + your filename. + + :Args: + - filename: The full path you wish to save your screenshot to. This + should end with a `.png` extension. + + :Usage: + :: + + driver.save_full_page_screenshot('/Screenshots/foo.png') + """ + return self.get_full_page_screenshot_as_file(filename) + + def get_full_page_screenshot_as_png(self): + """ + Gets the full document screenshot of the current window as a binary data. + + :Usage: + :: + + driver.get_full_page_screenshot_as_png() + """ + return base64.b64decode(self.get_full_page_screenshot_as_base64().encode('ascii')) + + def get_full_page_screenshot_as_base64(self): + """ + Gets the full document screenshot of the current window as a base64 encoded string + which is useful in embedded images in HTML. + + :Usage: + :: + + driver.get_full_page_screenshot_as_base64() + """ + return self.execute("FULL_PAGE_SCREENSHOT")['value'] diff --git a/selenium/webdriver/firefox/webelement.py b/selenium/webdriver/firefox/webelement.py new file mode 100644 index 00000000..4117ec06 --- /dev/null +++ b/selenium/webdriver/firefox/webelement.py @@ -0,0 +1,49 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.webelement import WebElement as RemoteWebElement + + +class FirefoxWebElement(RemoteWebElement): + + @property + def anonymous_children(self): + """Retrieve the anonymous children of this element in an XBL + context. This is only available in chrome context. + + See the `anonymous content documentation + <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XBL/XBL_1.0_Reference/Anonymous_Content>`_ + on MDN for more information. + + """ + return self._execute( + "ELEMENT_GET_ANONYMOUS_CHILDREN", + {"value": None}) + + def find_anonymous_element_by_attribute(self, name, value): + """Retrieve an anonymous descendant with a specified attribute + value. Typically used with an (arbitrary) anonid attribute to + retrieve a specific anonymous child in an XBL binding. + + See the `anonymous content documentation + <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XBL/XBL_1.0_Reference/Anonymous_Content>`_ + on MDN for more information. + + """ + return self._execute( + "ELEMENT_FIND_ANONYMOUS_ELEMENTS_BY_ATTRIBUTE", + {"name": name, "value": value})["value"] diff --git a/selenium/webdriver/ie/__init__.py b/selenium/webdriver/ie/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/ie/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/ie/options.py b/selenium/webdriver/ie/options.py new file mode 100644 index 00000000..672b8f6c --- /dev/null +++ b/selenium/webdriver/ie/options.py @@ -0,0 +1,338 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.options import ArgOptions + + +class ElementScrollBehavior(object): + TOP = 0 + BOTTOM = 1 + + +class Options(ArgOptions): + + KEY = 'se:ieOptions' + SWITCHES = 'ie.browserCommandLineSwitches' + + BROWSER_ATTACH_TIMEOUT = 'browserAttachTimeout' + ELEMENT_SCROLL_BEHAVIOR = 'elementScrollBehavior' + ENSURE_CLEAN_SESSION = 'ie.ensureCleanSession' + FILE_UPLOAD_DIALOG_TIMEOUT = 'ie.fileUploadDialogTimeout' + FORCE_CREATE_PROCESS_API = 'ie.forceCreateProcessApi' + FORCE_SHELL_WINDOWS_API = 'ie.forceShellWindowsApi' + FULL_PAGE_SCREENSHOT = 'ie.enableFullPageScreenshot' + IGNORE_PROTECTED_MODE_SETTINGS = 'ignoreProtectedModeSettings' + IGNORE_ZOOM_LEVEL = 'ignoreZoomSetting' + INITIAL_BROWSER_URL = 'initialBrowserUrl' + NATIVE_EVENTS = 'nativeEvents' + PERSISTENT_HOVER = 'enablePersistentHover' + REQUIRE_WINDOW_FOCUS = 'requireWindowFocus' + USE_PER_PROCESS_PROXY = 'ie.usePerProcessProxy' + VALIDATE_COOKIE_DOCUMENT_TYPE = 'ie.validateCookieDocumentType' + + def __init__(self): + super(Options, self).__init__() + self._options = {} + self._additional = {} + + @property + def options(self): + """:Returns: A dictionary of browser options """ + return self._options + + @property + def browser_attach_timeout(self): + """ + :Returns: The options Browser Attach Timeout in milliseconds + """ + return self._options.get(self.BROWSER_ATTACH_TIMEOUT) + + @browser_attach_timeout.setter + def browser_attach_timeout(self, value): + """ + Sets the options Browser Attach Timeout + + :Args: + - value: Timeout in milliseconds + + """ + if not isinstance(value, int): + raise ValueError('Browser Attach Timeout must be an integer.') + self._options[self.BROWSER_ATTACH_TIMEOUT] = value + + @property + def element_scroll_behavior(self): + """:Returns: The options Element Scroll Behavior in milliseconds """ + return self._options.get(self.ELEMENT_SCROLL_BEHAVIOR) + + @element_scroll_behavior.setter + def element_scroll_behavior(self, value): + """ + Sets the options Element Scroll Behavior + + :Args: + - value: 0 - Top, 1 - Bottom + + """ + if value not in [ElementScrollBehavior.TOP, ElementScrollBehavior.BOTTOM]: + raise ValueError('Element Scroll Behavior out of range.') + self._options[self.ELEMENT_SCROLL_BEHAVIOR] = value + + @property + def ensure_clean_session(self): + """:Returns: The options Ensure Clean Session value """ + return self._options.get(self.ENSURE_CLEAN_SESSION) + + @ensure_clean_session.setter + def ensure_clean_session(self, value): + """ + Sets the options Ensure Clean Session value + + :Args: + - value: boolean value + + """ + self._options[self.ENSURE_CLEAN_SESSION] = value + + @property + def file_upload_dialog_timeout(self): + """:Returns: The options File Upload Dialog Timeout in milliseconds """ + return self._options.get(self.FILE_UPLOAD_DIALOG_TIMEOUT) + + @file_upload_dialog_timeout.setter + def file_upload_dialog_timeout(self, value): + """ + Sets the options File Upload Dialog Timeout value + + :Args: + - value: Timeout in milliseconds + + """ + if not isinstance(value, int): + raise ValueError('File Upload Dialog Timeout must be an integer.') + self._options[self.FILE_UPLOAD_DIALOG_TIMEOUT] = value + + @property + def force_create_process_api(self): + """:Returns: The options Force Create Process Api value """ + return self._options.get(self.FORCE_CREATE_PROCESS_API) + + @force_create_process_api.setter + def force_create_process_api(self, value): + """ + Sets the options Force Create Process Api value + + :Args: + - value: boolean value + + """ + self._options[self.FORCE_CREATE_PROCESS_API] = value + + @property + def force_shell_windows_api(self): + """:Returns: The options Force Shell Windows Api value """ + return self._options.get(self.FORCE_SHELL_WINDOWS_API) + + @force_shell_windows_api.setter + def force_shell_windows_api(self, value): + """ + Sets the options Force Shell Windows Api value + + :Args: + - value: boolean value + + """ + self._options[self.FORCE_SHELL_WINDOWS_API] = value + + @property + def full_page_screenshot(self): + """:Returns: The options Full Page Screenshot value """ + return self._options.get(self.FULL_PAGE_SCREENSHOT) + + @full_page_screenshot.setter + def full_page_screenshot(self, value): + """ + Sets the options Full Page Screenshot value + + :Args: + - value: boolean value + + """ + self._options[self.FULL_PAGE_SCREENSHOT] = value + + @property + def ignore_protected_mode_settings(self): + """:Returns: The options Ignore Protected Mode Settings value """ + return self._options.get(self.IGNORE_PROTECTED_MODE_SETTINGS) + + @ignore_protected_mode_settings.setter + def ignore_protected_mode_settings(self, value): + """ + Sets the options Ignore Protected Mode Settings value + + :Args: + - value: boolean value + + """ + self._options[self.IGNORE_PROTECTED_MODE_SETTINGS] = value + + @property + def ignore_zoom_level(self): + """:Returns: The options Ignore Zoom Level value """ + return self._options.get(self.IGNORE_ZOOM_LEVEL) + + @ignore_zoom_level.setter + def ignore_zoom_level(self, value): + """ + Sets the options Ignore Zoom Level value + + :Args: + - value: boolean value + + """ + self._options[self.IGNORE_ZOOM_LEVEL] = value + + @property + def initial_browser_url(self): + """:Returns: The options Initial Browser Url value """ + return self._options.get(self.INITIAL_BROWSER_URL) + + @initial_browser_url.setter + def initial_browser_url(self, value): + """ + Sets the options Initial Browser Url value + + :Args: + - value: URL string + + """ + self._options[self.INITIAL_BROWSER_URL] = value + + @property + def native_events(self): + """:Returns: The options Native Events value """ + return self._options.get(self.NATIVE_EVENTS) + + @native_events.setter + def native_events(self, value): + """ + Sets the options Native Events value + + :Args: + - value: boolean value + + """ + self._options[self.NATIVE_EVENTS] = value + + @property + def persistent_hover(self): + """:Returns: The options Persistent Hover value """ + return self._options.get(self.PERSISTENT_HOVER) + + @persistent_hover.setter + def persistent_hover(self, value): + """ + Sets the options Persistent Hover value + + :Args: + - value: boolean value + + """ + self._options[self.PERSISTENT_HOVER] = value + + @property + def require_window_focus(self): + """:Returns: The options Require Window Focus value """ + return self._options.get(self.REQUIRE_WINDOW_FOCUS) + + @require_window_focus.setter + def require_window_focus(self, value): + """ + Sets the options Require Window Focus value + + :Args: + - value: boolean value + + """ + self._options[self.REQUIRE_WINDOW_FOCUS] = value + + @property + def use_per_process_proxy(self): + """:Returns: The options User Per Process Proxy value """ + return self._options.get(self.USE_PER_PROCESS_PROXY) + + @use_per_process_proxy.setter + def use_per_process_proxy(self, value): + """ + Sets the options User Per Process Proxy value + + :Args: + - value: boolean value + + """ + self._options[self.USE_PER_PROCESS_PROXY] = value + + @property + def validate_cookie_document_type(self): + """:Returns: The options Validate Cookie Document Type value """ + return self._options.get(self.VALIDATE_COOKIE_DOCUMENT_TYPE) + + @validate_cookie_document_type.setter + def validate_cookie_document_type(self, value): + """ + Sets the options Validate Cookie Document Type value + + :Args: + - value: boolean value + + """ + self._options[self.VALIDATE_COOKIE_DOCUMENT_TYPE] = value + + @property + def additional_options(self): + """:Returns: The additional options """ + return self._additional + + def add_additional_option(self, name, value): + """ + Adds an additional option not yet added as a safe option for IE + + :Args: + - name: name of the option to add + - value: value of the option to add + + """ + self._additional[name] = value + + def to_capabilities(self): + """Marshals the IE options to the correct object.""" + caps = self._caps + + opts = self._options.copy() + if len(self._arguments) > 0: + opts[self.SWITCHES] = ' '.join(self._arguments) + + if len(self._additional) > 0: + opts.update(self._additional) + + if len(opts) > 0: + caps[Options.KEY] = opts + return caps + + @property + def default_capabilities(self): + return DesiredCapabilities.INTERNETEXPLORER.copy() diff --git a/selenium/webdriver/ie/service.py b/selenium/webdriver/ie/service.py new file mode 100644 index 00000000..c9083e16 --- /dev/null +++ b/selenium/webdriver/ie/service.py @@ -0,0 +1,50 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the IEDriver + """ + + def __init__(self, executable_path, port=0, host=None, log_level=None, log_file=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the IEDriver + - port : Port the service is running on + - host : IP address the service port is bound + - log_level : Level of logging of service, may be "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE". + Default is "FATAL". + - log_file : Target of logging of service, may be "stdout", "stderr" or file path. + Default is "stdout".""" + self.service_args = [] + if host is not None: + self.service_args.append("--host=%s" % host) + if log_level is not None: + self.service_args.append("--log-level=%s" % log_level) + if log_file is not None: + self.service_args.append("--log-file=%s" % log_file) + + service.Service.__init__(self, executable_path, port=port, + start_error_message="Please download from http://selenium-release.storage.googleapis.com/index.html and read up at https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver") + + def command_line_args(self): + return ["--port=%d" % self.port] + self.service_args diff --git a/selenium/webdriver/ie/webdriver.py b/selenium/webdriver/ie/webdriver.py new file mode 100644 index 00000000..c15fc0cc --- /dev/null +++ b/selenium/webdriver/ie/webdriver.py @@ -0,0 +1,115 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import warnings + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .service import Service +from .options import Options + +DEFAULT_TIMEOUT = 30 +DEFAULT_PORT = 0 +DEFAULT_HOST = None +DEFAULT_LOG_LEVEL = None +DEFAULT_SERVICE_LOG_PATH = None + + +class WebDriver(RemoteWebDriver): + """ Controls the IEServerDriver and allows you to drive Internet Explorer """ + + def __init__(self, executable_path='IEDriverServer.exe', capabilities=None, + port=DEFAULT_PORT, timeout=DEFAULT_TIMEOUT, host=DEFAULT_HOST, + log_level=DEFAULT_LOG_LEVEL, service_log_path=DEFAULT_SERVICE_LOG_PATH, + options=None, service=None, + desired_capabilities=None, keep_alive=False): + """ + Creates a new instance of the chrome driver. + + Starts the service and then creates new instance of chrome driver. + + :Args: + - executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH + - capabilities - Deprecated: capabilities Dictionary object + - port - Deprecated: port you would like the service to run, if left as 0, a free port will be found. + - timeout - Deprecated: no longer used, kept for backward compatibility + - host - Deprecated: IP address for the service + - log_level - Deprecated: log level you would like the service to run. + - service_log_path - Deprecated: target of logging of service, may be "stdout", "stderr" or file path. + - options - IE Options instance, providing additional IE options + - desired_capabilities - Deprecated: alias of capabilities; this will make the signature consistent with RemoteWebDriver. + - keep_alive - Whether to configure RemoteConnection to use HTTP keep-alive. + """ + if executable_path != 'IEDriverServer.exe': + warnings.warn('executable_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if capabilities is not None: + warnings.warn('capabilities has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if port != DEFAULT_PORT: + warnings.warn('port has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.port = port + if timeout != DEFAULT_TIMEOUT: + warnings.warn('timeout has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if host != DEFAULT_HOST: + warnings.warn('host has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + self.host = host + if log_level != DEFAULT_LOG_LEVEL: + warnings.warn('log_level has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + if service_log_path != DEFAULT_SERVICE_LOG_PATH: + warnings.warn('service_log_path has been deprecated, please pass in a Service object', + DeprecationWarning, stacklevel=2) + + # If both capabilities and desired capabilities are set, ignore desired capabilities. + if capabilities is None and desired_capabilities: + capabilities = desired_capabilities + + if options is None: + if capabilities is None: + capabilities = self.create_options().to_capabilities() + else: + if capabilities is None: + capabilities = options.to_capabilities() + else: + # desired_capabilities stays as passed in + capabilities.update(options.to_capabilities()) + if service is None: + service = Service() + self.iedriver = Service( + executable_path, + port=self.port, + host=self.host, + log_level=log_level, + log_file=service_log_path) + + self.iedriver.start() + + RemoteWebDriver.__init__( + self, + command_executor='http://localhost:%d' % self.port, + desired_capabilities=capabilities, + keep_alive=keep_alive) + self._is_remote = False + + def quit(self): + RemoteWebDriver.quit(self) + self.iedriver.stop() + + def create_options(self): + return Options() diff --git a/selenium/webdriver/opera/__init__.py b/selenium/webdriver/opera/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/opera/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/opera/options.py b/selenium/webdriver/opera/options.py new file mode 100644 index 00000000..4fc29c17 --- /dev/null +++ b/selenium/webdriver/opera/options.py @@ -0,0 +1,109 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +class Options(ChromeOptions): + KEY = "operaOptions" + + def __init__(self): + ChromeOptions.__init__(self) + self._android_package_name = '' + self._android_device_socket = '' + self._android_command_line_file = '' + + @property + def android_package_name(self): + """ + :Returns: The name of the Opera package + """ + return self._android_package_name + + @android_package_name.setter + def android_package_name(self, value): + """ + Allows you to set the package name + + :Args: + - value: devtools socket name + """ + self._android_package_name = value + + @property + def android_device_socket(self): + """ + :Returns: The name of the devtools socket + """ + return self._android_device_socket + + @android_device_socket.setter + def android_device_socket(self, value): + """ + Allows you to set the devtools socket name + + :Args: + - value: devtools socket name + """ + self._android_device_socket = value + + @property + def android_command_line_file(self): + """ + :Returns: The path of the command line file + """ + return self._android_command_line_file + + @android_command_line_file.setter + def android_command_line_file(self, value): + """ + Allows you to set where the command line file lives + + :Args: + - value: command line file path + """ + self._android_command_line_file = value + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + returns a dictionary with everything + """ + capabilities = ChromeOptions.to_capabilities(self) + capabilities.update(self._caps) + opera_options = capabilities[self.KEY] + + if self.android_package_name: + opera_options["androidPackage"] = self.android_package_name + if self.android_device_socket: + opera_options["androidDeviceSocket"] = self.android_device_socket + if self.android_command_line_file: + opera_options["androidCommandLineFile"] = \ + self.android_command_line_file + return capabilities + + @property + def default_capabilities(self): + return DesiredCapabilities.OPERA.copy() + + +class AndroidOptions(Options): + + def __init__(self): + Options.__init__(self) + self.android_package_name = 'com.opera.browser' diff --git a/selenium/webdriver/opera/webdriver.py b/selenium/webdriver/opera/webdriver.py new file mode 100644 index 00000000..291e603a --- /dev/null +++ b/selenium/webdriver/opera/webdriver.py @@ -0,0 +1,75 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from selenium.webdriver.chrome.webdriver import WebDriver as ChromiumDriver +from .options import Options + + +class OperaDriver(ChromiumDriver): + """Controls the new OperaDriver and allows you + to drive the Opera browser based on Chromium.""" + + def __init__(self, executable_path=None, port=0, + options=None, service_args=None, + desired_capabilities=None, service_log_path=None, + opera_options=None, keep_alive=True): + """ + Creates a new instance of the operadriver. + + Starts the service and then creates new instance of operadriver. + + :Args: + - executable_path - path to the executable. If the default is used + it assumes the executable is in the $PATH + - port - port you would like the service to run, if left as 0, + a free port will be found. + - options: this takes an instance of OperaOptions + - service_args - List of args to pass to the driver service + - desired_capabilities: Dictionary object with non-browser specific + - service_log_path - Where to log information from the driver. + capabilities only, such as "proxy" or "loggingPref". + """ + executable_path = (executable_path if executable_path is not None + else "operadriver") + ChromiumDriver.__init__(self, + executable_path=executable_path, + port=port, + options=options, + service_args=service_args, + desired_capabilities=desired_capabilities, + service_log_path=service_log_path, + keep_alive=keep_alive) + + def create_options(self): + return Options() + + +class WebDriver(OperaDriver): + class ServiceType: + CHROMIUM = 2 + + def __init__(self, + desired_capabilities=None, + executable_path=None, + port=0, + service_log_path=None, + service_args=None, + options=None): + OperaDriver.__init__(self, executable_path=executable_path, + port=port, options=options, + service_args=service_args, + desired_capabilities=desired_capabilities, + service_log_path=service_log_path) diff --git a/selenium/webdriver/phantomjs/__init__.py b/selenium/webdriver/phantomjs/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/phantomjs/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/phantomjs/service.py b/selenium/webdriver/phantomjs/service.py new file mode 100644 index 00000000..37b4e9f8 --- /dev/null +++ b/selenium/webdriver/phantomjs/service.py @@ -0,0 +1,68 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import os +import tempfile +from selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of PhantomJS / Ghostdriver + """ + + def __init__(self, executable_path, port=0, service_args=None, log_path=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to PhantomJS binary + - port : Port the service is running on + - service_args : A List of other command line options to pass to PhantomJS + - log_path: Path for PhantomJS service to log to + """ + self.service_args = service_args + if self.service_args is None: + self.service_args = [] + else: + self.service_args = service_args[:] + if not log_path: + log_path = "ghostdriver.log" + if not self._args_contain("--cookies-file="): + self._cookie_temp_file_handle, self._cookie_temp_file = tempfile.mkstemp() + self.service_args.append("--cookies-file=" + self._cookie_temp_file) + else: + self._cookie_temp_file = None + + service.Service.__init__(self, executable_path, port=port, log_file=open(log_path, 'w')) + + def _args_contain(self, arg): + return len(list(filter(lambda x: x.startswith(arg), self.service_args))) > 0 + + def command_line_args(self): + return self.service_args + ["--webdriver=%d" % self.port] + + @property + def service_url(self): + """ + Gets the url of the GhostDriver Service + """ + return "http://localhost:%d/wd/hub" % self.port + + def send_remote_shutdown_command(self): + if self._cookie_temp_file: + os.close(self._cookie_temp_file_handle) + os.remove(self._cookie_temp_file) diff --git a/selenium/webdriver/phantomjs/webdriver.py b/selenium/webdriver/phantomjs/webdriver.py new file mode 100644 index 00000000..e07024f7 --- /dev/null +++ b/selenium/webdriver/phantomjs/webdriver.py @@ -0,0 +1,80 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import warnings + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .service import Service + + +class WebDriver(RemoteWebDriver): + """ + Wrapper to communicate with PhantomJS through Ghostdriver. + + You will need to follow all the directions here: + https://github.com/detro/ghostdriver + """ + + def __init__(self, executable_path="phantomjs", + port=0, desired_capabilities=DesiredCapabilities.PHANTOMJS, + service_args=None, service_log_path=None): + """ + Creates a new instance of the PhantomJS / Ghostdriver. + + Starts the service and then creates new instance of the driver. + + :Args: + - executable_path - path to the executable. If the default is used it assumes the executable is in the $PATH + - port - port you would like the service to run, if left as 0, a free port will be found. + - desired_capabilities: Dictionary object with non-browser specific + capabilities only, such as "proxy" or "loggingPref". + - service_args : A List of command line arguments to pass to PhantomJS + - service_log_path: Path for phantomjs service to log to. + """ + warnings.warn('Selenium support for PhantomJS has been deprecated, please use headless ' + 'versions of Chrome or Firefox instead') + self.service = Service( + executable_path, + port=port, + service_args=service_args, + log_path=service_log_path) + self.service.start() + + try: + RemoteWebDriver.__init__( + self, + command_executor=self.service.service_url, + desired_capabilities=desired_capabilities) + except Exception: + self.quit() + raise + + self._is_remote = False + + def quit(self): + """ + Closes the browser and shuts down the PhantomJS executable + that is started when starting the PhantomJS + """ + try: + RemoteWebDriver.quit(self) + except Exception: + # We don't care about the message because something probably has gone wrong + pass + finally: + self.service.stop() diff --git a/selenium/webdriver/remote/__init__.py b/selenium/webdriver/remote/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/remote/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/remote/command.py b/selenium/webdriver/remote/command.py new file mode 100644 index 00000000..053e47b7 --- /dev/null +++ b/selenium/webdriver/remote/command.py @@ -0,0 +1,174 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +class Command(object): + """ + Defines constants for the standard WebDriver commands. + + While these constants have no meaning in and of themselves, they are + used to marshal commands through a service that implements WebDriver's + remote wire protocol: + + https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol + + """ + + # Keep in sync with org.openqa.selenium.remote.DriverCommand + + STATUS = "status" + NEW_SESSION = "newSession" + GET_ALL_SESSIONS = "getAllSessions" + DELETE_SESSION = "deleteSession" + NEW_WINDOW = "newWindow" + CLOSE = "close" + QUIT = "quit" + GET = "get" + GO_BACK = "goBack" + GO_FORWARD = "goForward" + REFRESH = "refresh" + ADD_COOKIE = "addCookie" + GET_COOKIE = "getCookie" + GET_ALL_COOKIES = "getCookies" + DELETE_COOKIE = "deleteCookie" + DELETE_ALL_COOKIES = "deleteAllCookies" + FIND_ELEMENT = "findElement" + FIND_ELEMENTS = "findElements" + FIND_CHILD_ELEMENT = "findChildElement" + FIND_CHILD_ELEMENTS = "findChildElements" + CLEAR_ELEMENT = "clearElement" + CLICK_ELEMENT = "clickElement" + SEND_KEYS_TO_ELEMENT = "sendKeysToElement" + SEND_KEYS_TO_ACTIVE_ELEMENT = "sendKeysToActiveElement" + SUBMIT_ELEMENT = "submitElement" + UPLOAD_FILE = "uploadFile" + GET_CURRENT_WINDOW_HANDLE = "getCurrentWindowHandle" + W3C_GET_CURRENT_WINDOW_HANDLE = "w3cGetCurrentWindowHandle" + GET_WINDOW_HANDLES = "getWindowHandles" + W3C_GET_WINDOW_HANDLES = "w3cGetWindowHandles" + GET_WINDOW_SIZE = "getWindowSize" + W3C_GET_WINDOW_SIZE = "w3cGetWindowSize" + W3C_GET_WINDOW_POSITION = "w3cGetWindowPosition" + GET_WINDOW_POSITION = "getWindowPosition" + SET_WINDOW_SIZE = "setWindowSize" + W3C_SET_WINDOW_SIZE = "w3cSetWindowSize" + SET_WINDOW_RECT = "setWindowRect" + GET_WINDOW_RECT = "getWindowRect" + SET_WINDOW_POSITION = "setWindowPosition" + W3C_SET_WINDOW_POSITION = "w3cSetWindowPosition" + SWITCH_TO_WINDOW = "switchToWindow" + SWITCH_TO_FRAME = "switchToFrame" + SWITCH_TO_PARENT_FRAME = "switchToParentFrame" + GET_ACTIVE_ELEMENT = "getActiveElement" + W3C_GET_ACTIVE_ELEMENT = "w3cGetActiveElement" + GET_CURRENT_URL = "getCurrentUrl" + GET_PAGE_SOURCE = "getPageSource" + GET_TITLE = "getTitle" + EXECUTE_SCRIPT = "executeScript" + W3C_EXECUTE_SCRIPT = "w3cExecuteScript" + W3C_EXECUTE_SCRIPT_ASYNC = "w3cExecuteScriptAsync" + GET_ELEMENT_TEXT = "getElementText" + GET_ELEMENT_VALUE = "getElementValue" + GET_ELEMENT_TAG_NAME = "getElementTagName" + SET_ELEMENT_SELECTED = "setElementSelected" + IS_ELEMENT_SELECTED = "isElementSelected" + IS_ELEMENT_ENABLED = "isElementEnabled" + IS_ELEMENT_DISPLAYED = "isElementDisplayed" + GET_ELEMENT_LOCATION = "getElementLocation" + GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = "getElementLocationOnceScrolledIntoView" + GET_ELEMENT_SIZE = "getElementSize" + GET_ELEMENT_RECT = "getElementRect" + GET_ELEMENT_ATTRIBUTE = "getElementAttribute" + GET_ELEMENT_PROPERTY = "getElementProperty" + GET_ELEMENT_VALUE_OF_CSS_PROPERTY = "getElementValueOfCssProperty" + SCREENSHOT = "screenshot" + ELEMENT_SCREENSHOT = "elementScreenshot" + IMPLICIT_WAIT = "implicitlyWait" + EXECUTE_ASYNC_SCRIPT = "executeAsyncScript" + SET_SCRIPT_TIMEOUT = "setScriptTimeout" + SET_TIMEOUTS = "setTimeouts" + MAXIMIZE_WINDOW = "windowMaximize" + W3C_MAXIMIZE_WINDOW = "w3cMaximizeWindow" + GET_LOG = "getLog" + GET_AVAILABLE_LOG_TYPES = "getAvailableLogTypes" + FULLSCREEN_WINDOW = "fullscreenWindow" + MINIMIZE_WINDOW = "minimizeWindow" + + # Alerts + DISMISS_ALERT = "dismissAlert" + W3C_DISMISS_ALERT = "w3cDismissAlert" + ACCEPT_ALERT = "acceptAlert" + W3C_ACCEPT_ALERT = "w3cAcceptAlert" + SET_ALERT_VALUE = "setAlertValue" + W3C_SET_ALERT_VALUE = "w3cSetAlertValue" + GET_ALERT_TEXT = "getAlertText" + W3C_GET_ALERT_TEXT = "w3cGetAlertText" + SET_ALERT_CREDENTIALS = "setAlertCredentials" + + # Advanced user interactions + W3C_ACTIONS = "actions" + W3C_CLEAR_ACTIONS = "clearActionState" + CLICK = "mouseClick" + DOUBLE_CLICK = "mouseDoubleClick" + MOUSE_DOWN = "mouseButtonDown" + MOUSE_UP = "mouseButtonUp" + MOVE_TO = "mouseMoveTo" + + # Screen Orientation + SET_SCREEN_ORIENTATION = "setScreenOrientation" + GET_SCREEN_ORIENTATION = "getScreenOrientation" + + # Touch Actions + SINGLE_TAP = "touchSingleTap" + TOUCH_DOWN = "touchDown" + TOUCH_UP = "touchUp" + TOUCH_MOVE = "touchMove" + TOUCH_SCROLL = "touchScroll" + DOUBLE_TAP = "touchDoubleTap" + LONG_PRESS = "touchLongPress" + FLICK = "touchFlick" + + # HTML 5 + EXECUTE_SQL = "executeSql" + + GET_LOCATION = "getLocation" + SET_LOCATION = "setLocation" + + GET_APP_CACHE = "getAppCache" + GET_APP_CACHE_STATUS = "getAppCacheStatus" + CLEAR_APP_CACHE = "clearAppCache" + + GET_LOCAL_STORAGE_ITEM = "getLocalStorageItem" + REMOVE_LOCAL_STORAGE_ITEM = "removeLocalStorageItem" + GET_LOCAL_STORAGE_KEYS = "getLocalStorageKeys" + SET_LOCAL_STORAGE_ITEM = "setLocalStorageItem" + CLEAR_LOCAL_STORAGE = "clearLocalStorage" + GET_LOCAL_STORAGE_SIZE = "getLocalStorageSize" + + GET_SESSION_STORAGE_ITEM = "getSessionStorageItem" + REMOVE_SESSION_STORAGE_ITEM = "removeSessionStorageItem" + GET_SESSION_STORAGE_KEYS = "getSessionStorageKeys" + SET_SESSION_STORAGE_ITEM = "setSessionStorageItem" + CLEAR_SESSION_STORAGE = "clearSessionStorage" + GET_SESSION_STORAGE_SIZE = "getSessionStorageSize" + + # Mobile + GET_NETWORK_CONNECTION = "getNetworkConnection" + SET_NETWORK_CONNECTION = "setNetworkConnection" + CURRENT_CONTEXT_HANDLE = "getCurrentContextHandle" + CONTEXT_HANDLES = "getContextHandles" + SWITCH_TO_CONTEXT = "switchToContext" diff --git a/selenium/webdriver/remote/errorhandler.py b/selenium/webdriver/remote/errorhandler.py new file mode 100644 index 00000000..b74da3e0 --- /dev/null +++ b/selenium/webdriver/remote/errorhandler.py @@ -0,0 +1,242 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.common.exceptions import (ElementClickInterceptedException, + ElementNotInteractableException, + ElementNotSelectableException, + ElementNotVisibleException, + InsecureCertificateException, + InvalidCoordinatesException, + InvalidElementStateException, + InvalidSessionIdException, + InvalidSelectorException, + ImeNotAvailableException, + ImeActivationFailedException, + InvalidArgumentException, + InvalidCookieDomainException, + JavascriptException, + MoveTargetOutOfBoundsException, + NoSuchCookieException, + NoSuchElementException, + NoSuchFrameException, + NoSuchWindowException, + NoAlertPresentException, + ScreenshotException, + SessionNotCreatedException, + StaleElementReferenceException, + TimeoutException, + UnableToSetCookieException, + UnexpectedAlertPresentException, + UnknownMethodException, + WebDriverException) + +try: + basestring +except NameError: # Python 3.x + basestring = str + + +class ErrorCode(object): + """ + Error codes defined in the WebDriver wire protocol. + """ + # Keep in sync with org.openqa.selenium.remote.ErrorCodes and errorcodes.h + SUCCESS = 0 + NO_SUCH_ELEMENT = [7, 'no such element'] + NO_SUCH_FRAME = [8, 'no such frame'] + UNKNOWN_COMMAND = [9, 'unknown command'] + STALE_ELEMENT_REFERENCE = [10, 'stale element reference'] + ELEMENT_NOT_VISIBLE = [11, 'element not visible'] + INVALID_ELEMENT_STATE = [12, 'invalid element state'] + UNKNOWN_ERROR = [13, 'unknown error'] + ELEMENT_IS_NOT_SELECTABLE = [15, 'element not selectable'] + JAVASCRIPT_ERROR = [17, 'javascript error'] + XPATH_LOOKUP_ERROR = [19, 'invalid selector'] + TIMEOUT = [21, 'timeout'] + NO_SUCH_WINDOW = [23, 'no such window'] + INVALID_COOKIE_DOMAIN = [24, 'invalid cookie domain'] + UNABLE_TO_SET_COOKIE = [25, 'unable to set cookie'] + UNEXPECTED_ALERT_OPEN = [26, 'unexpected alert open'] + NO_ALERT_OPEN = [27, 'no such alert'] + SCRIPT_TIMEOUT = [28, 'script timeout'] + INVALID_ELEMENT_COORDINATES = [29, 'invalid element coordinates'] + IME_NOT_AVAILABLE = [30, 'ime not available'] + IME_ENGINE_ACTIVATION_FAILED = [31, 'ime engine activation failed'] + INVALID_SELECTOR = [32, 'invalid selector'] + SESSION_NOT_CREATED = [33, 'session not created'] + MOVE_TARGET_OUT_OF_BOUNDS = [34, 'move target out of bounds'] + INVALID_XPATH_SELECTOR = [51, 'invalid selector'] + INVALID_XPATH_SELECTOR_RETURN_TYPER = [52, 'invalid selector'] + + ELEMENT_NOT_INTERACTABLE = [60, 'element not interactable'] + INSECURE_CERTIFICATE = ['insecure certificate'] + INVALID_ARGUMENT = [61, 'invalid argument'] + INVALID_COORDINATES = ['invalid coordinates'] + INVALID_SESSION_ID = ['invalid session id'] + NO_SUCH_COOKIE = [62, 'no such cookie'] + UNABLE_TO_CAPTURE_SCREEN = [63, 'unable to capture screen'] + ELEMENT_CLICK_INTERCEPTED = [64, 'element click intercepted'] + UNKNOWN_METHOD = ['unknown method exception'] + + METHOD_NOT_ALLOWED = [405, 'unsupported operation'] + + +class ErrorHandler(object): + """ + Handles errors returned by the WebDriver server. + """ + def check_response(self, response): + """ + Checks that a JSON response from the WebDriver does not have an error. + + :Args: + - response - The JSON response from the WebDriver server as a dictionary + object. + + :Raises: If the response contains an error message. + """ + status = response.get('status', None) + if status is None or status == ErrorCode.SUCCESS: + return + value = None + message = response.get("message", "") + screen = response.get("screen", "") + stacktrace = None + if isinstance(status, int): + value_json = response.get('value', None) + if value_json and isinstance(value_json, basestring): + import json + try: + value = json.loads(value_json) + if len(value.keys()) == 1: + value = value['value'] + status = value.get('error', None) + if status is None: + status = value["status"] + message = value["value"] + if not isinstance(message, basestring): + value = message + message = message.get('message') + else: + message = value.get('message', None) + except ValueError: + pass + + if status in ErrorCode.NO_SUCH_ELEMENT: + exception_class = NoSuchElementException + elif status in ErrorCode.NO_SUCH_FRAME: + exception_class = NoSuchFrameException + elif status in ErrorCode.NO_SUCH_WINDOW: + exception_class = NoSuchWindowException + elif status in ErrorCode.STALE_ELEMENT_REFERENCE: + exception_class = StaleElementReferenceException + elif status in ErrorCode.ELEMENT_NOT_VISIBLE: + exception_class = ElementNotVisibleException + elif status in ErrorCode.INVALID_ELEMENT_STATE: + exception_class = InvalidElementStateException + elif status in ErrorCode.INVALID_SELECTOR \ + or status in ErrorCode.INVALID_XPATH_SELECTOR \ + or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER: + exception_class = InvalidSelectorException + elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE: + exception_class = ElementNotSelectableException + elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE: + exception_class = ElementNotInteractableException + elif status in ErrorCode.INVALID_COOKIE_DOMAIN: + exception_class = InvalidCookieDomainException + elif status in ErrorCode.UNABLE_TO_SET_COOKIE: + exception_class = UnableToSetCookieException + elif status in ErrorCode.TIMEOUT: + exception_class = TimeoutException + elif status in ErrorCode.SCRIPT_TIMEOUT: + exception_class = TimeoutException + elif status in ErrorCode.UNKNOWN_ERROR: + exception_class = WebDriverException + elif status in ErrorCode.UNEXPECTED_ALERT_OPEN: + exception_class = UnexpectedAlertPresentException + elif status in ErrorCode.NO_ALERT_OPEN: + exception_class = NoAlertPresentException + elif status in ErrorCode.IME_NOT_AVAILABLE: + exception_class = ImeNotAvailableException + elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED: + exception_class = ImeActivationFailedException + elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS: + exception_class = MoveTargetOutOfBoundsException + elif status in ErrorCode.JAVASCRIPT_ERROR: + exception_class = JavascriptException + elif status in ErrorCode.SESSION_NOT_CREATED: + exception_class = SessionNotCreatedException + elif status in ErrorCode.INVALID_ARGUMENT: + exception_class = InvalidArgumentException + elif status in ErrorCode.NO_SUCH_COOKIE: + exception_class = NoSuchCookieException + elif status in ErrorCode.UNABLE_TO_CAPTURE_SCREEN: + exception_class = ScreenshotException + elif status in ErrorCode.ELEMENT_CLICK_INTERCEPTED: + exception_class = ElementClickInterceptedException + elif status in ErrorCode.INSECURE_CERTIFICATE: + exception_class = InsecureCertificateException + elif status in ErrorCode.INVALID_COORDINATES: + exception_class = InvalidCoordinatesException + elif status in ErrorCode.INVALID_SESSION_ID: + exception_class = InvalidSessionIdException + elif status in ErrorCode.UNKNOWN_METHOD: + exception_class = UnknownMethodException + else: + exception_class = WebDriverException + if value == '' or value is None: + value = response['value'] + if isinstance(value, basestring): + raise exception_class(value) + if message == "" and 'message' in value: + message = value['message'] + + screen = None + if 'screen' in value: + screen = value['screen'] + + stacktrace = None + st_value = value.get('stackTrace') or value.get('stacktrace') + if st_value: + if isinstance(st_value, basestring): + st_value = st_value.split('\n') + stacktrace = [] + try: + for frame in st_value: + line = self._value_or_default(frame, 'lineNumber', '') + file = self._value_or_default(frame, 'fileName', '<anonymous>') + if line: + file = "%s:%s" % (file, line) + meth = self._value_or_default(frame, 'methodName', '<anonymous>') + if 'className' in frame: + meth = "%s.%s" % (frame['className'], meth) + msg = " at %s (%s)" + msg = msg % (meth, file) + stacktrace.append(msg) + except TypeError: + pass + if exception_class == UnexpectedAlertPresentException: + alert_text = None + if 'data' in value: + alert_text = value['data'].get('text') + elif 'alert' in value: + alert_text = value['alert'].get('text') + raise exception_class(message, screen, stacktrace, alert_text) + raise exception_class(message, screen, stacktrace) + + def _value_or_default(self, obj, key, default): + return obj[key] if key in obj else default diff --git a/selenium/webdriver/remote/file_detector.py b/selenium/webdriver/remote/file_detector.py new file mode 100644 index 00000000..caec9c30 --- /dev/null +++ b/selenium/webdriver/remote/file_detector.py @@ -0,0 +1,58 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import abc +import os +from selenium.webdriver.common.utils import keys_to_typing + + +class FileDetector(object): + """ + Used for identifying whether a sequence of chars represents the path to a + file. + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def is_local_file(self, *keys): + return + + +class UselessFileDetector(FileDetector): + """ + A file detector that never finds anything. + """ + def is_local_file(self, *keys): + return None + + +class LocalFileDetector(FileDetector): + """ + Detects files on the local disk. + """ + def is_local_file(self, *keys): + file_path = ''.join(keys_to_typing(keys)) + + if not file_path: + return None + + try: + if os.path.isfile(file_path): + return file_path + except Exception: + pass + return None diff --git a/selenium/webdriver/remote/mobile.py b/selenium/webdriver/remote/mobile.py new file mode 100644 index 00000000..a0da2498 --- /dev/null +++ b/selenium/webdriver/remote/mobile.py @@ -0,0 +1,85 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from .command import Command + + +class Mobile(object): + + class ConnectionType(object): + + def __init__(self, mask): + self.mask = mask + + @property + def airplane_mode(self): + return self.mask % 2 == 1 + + @property + def wifi(self): + return (self.mask / 2) % 2 == 1 + + @property + def data(self): + return (self.mask / 4) > 0 + + ALL_NETWORK = ConnectionType(6) + WIFI_NETWORK = ConnectionType(2) + DATA_NETWORK = ConnectionType(4) + AIRPLANE_MODE = ConnectionType(1) + + def __init__(self, driver): + self._driver = driver + + @property + def network_connection(self): + return self.ConnectionType(self._driver.execute(Command.GET_NETWORK_CONNECTION)['value']) + + def set_network_connection(self, network): + """ + Set the network connection for the remote device. + + Example of setting airplane mode:: + + driver.mobile.set_network_connection(driver.mobile.AIRPLANE_MODE) + """ + mode = network.mask if isinstance(network, self.ConnectionType) else network + return self.ConnectionType(self._driver.execute( + Command.SET_NETWORK_CONNECTION, { + 'name': 'network_connection', + 'parameters': {'type': mode}})['value']) + + @property + def context(self): + """ + returns the current context (Native or WebView). + """ + return self._driver.execute(Command.CURRENT_CONTEXT_HANDLE) + + @property + def contexts(self): + """ + returns a list of available contexts + """ + return self._driver.execute(Command.CONTEXT_HANDLES) + + @context.setter + def context(self, new_context): + """ + sets the current context + """ + self._driver.execute(Command.SWITCH_TO_CONTEXT, {"name": new_context}) diff --git a/selenium/webdriver/remote/remote_connection.py b/selenium/webdriver/remote/remote_connection.py new file mode 100644 index 00000000..9098bb5f --- /dev/null +++ b/selenium/webdriver/remote/remote_connection.py @@ -0,0 +1,449 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import logging +import platform +import socket +import string + +import urllib3 + +try: + from urllib import parse +except ImportError: # above is available in py3+, below is py2.7 + import urlparse as parse + +from selenium.webdriver.common import utils as common_utils +from selenium import __version__ +from .command import Command +from .errorhandler import ErrorCode +from . import utils + +LOGGER = logging.getLogger(__name__) + + +class RemoteConnection(object): + """A connection with the Remote WebDriver server. + + Communicates with the server using the WebDriver wire protocol: + https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol""" + + _timeout = socket._GLOBAL_DEFAULT_TIMEOUT + + @classmethod + def get_timeout(cls): + """ + :Returns: + Timeout value in seconds for all http requests made to the Remote Connection + """ + return None if cls._timeout == socket._GLOBAL_DEFAULT_TIMEOUT else cls._timeout + + @classmethod + def set_timeout(cls, timeout): + """ + Override the default timeout + + :Args: + - timeout - timeout value for http requests in seconds + """ + cls._timeout = timeout + + @classmethod + def reset_timeout(cls): + """ + Reset the http request timeout to socket._GLOBAL_DEFAULT_TIMEOUT + """ + cls._timeout = socket._GLOBAL_DEFAULT_TIMEOUT + + @classmethod + def get_remote_connection_headers(cls, parsed_url, keep_alive=False): + """ + Get headers for remote request. + + :Args: + - parsed_url - The parsed url + - keep_alive (Boolean) - Is this a keep-alive connection (default: False) + """ + + system = platform.system().lower() + if system == "darwin": + system = "mac" + + headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json;charset=UTF-8', + 'User-Agent': 'selenium/{} (python {})'.format(__version__, system) + } + + if parsed_url.username: + base64string = base64.b64encode('{0.username}:{0.password}'.format(parsed_url).encode()) + headers.update({ + 'Authorization': 'Basic {}'.format(base64string.decode()) + }) + + if keep_alive: + headers.update({ + 'Connection': 'keep-alive' + }) + + return headers + + def __init__(self, remote_server_addr, keep_alive=False, resolve_ip=True): + # Attempt to resolve the hostname and get an IP address. + self.keep_alive = keep_alive + parsed_url = parse.urlparse(remote_server_addr) + if parsed_url.hostname and resolve_ip: + port = parsed_url.port or None + if parsed_url.scheme == "https": + ip = parsed_url.hostname + elif port and not common_utils.is_connectable(port, parsed_url.hostname): + ip = None + LOGGER.info('Could not connect to port {} on host ' + '{}'.format(port, parsed_url.hostname)) + else: + ip = common_utils.find_connectable_ip(parsed_url.hostname, + port=port) + if ip: + netloc = ip + if parsed_url.port: + netloc = common_utils.join_host_port(netloc, + parsed_url.port) + if parsed_url.username: + auth = parsed_url.username + if parsed_url.password: + auth += ':%s' % parsed_url.password + netloc = '%s@%s' % (auth, netloc) + remote_server_addr = parse.urlunparse( + (parsed_url.scheme, netloc, parsed_url.path, + parsed_url.params, parsed_url.query, parsed_url.fragment)) + else: + LOGGER.info('Could not get IP address for host: %s' % + parsed_url.hostname) + + self._url = remote_server_addr + if keep_alive: + self._conn = urllib3.PoolManager(timeout=self._timeout) + + self._commands = { + Command.STATUS: ('GET', '/status'), + Command.NEW_SESSION: ('POST', '/session'), + Command.GET_ALL_SESSIONS: ('GET', '/sessions'), + Command.QUIT: ('DELETE', '/session/$sessionId'), + Command.GET_CURRENT_WINDOW_HANDLE: + ('GET', '/session/$sessionId/window_handle'), + Command.W3C_GET_CURRENT_WINDOW_HANDLE: + ('GET', '/session/$sessionId/window'), + Command.GET_WINDOW_HANDLES: + ('GET', '/session/$sessionId/window_handles'), + Command.W3C_GET_WINDOW_HANDLES: + ('GET', '/session/$sessionId/window/handles'), + Command.GET: ('POST', '/session/$sessionId/url'), + Command.GO_FORWARD: ('POST', '/session/$sessionId/forward'), + Command.GO_BACK: ('POST', '/session/$sessionId/back'), + Command.REFRESH: ('POST', '/session/$sessionId/refresh'), + Command.EXECUTE_SCRIPT: ('POST', '/session/$sessionId/execute'), + Command.W3C_EXECUTE_SCRIPT: + ('POST', '/session/$sessionId/execute/sync'), + Command.W3C_EXECUTE_SCRIPT_ASYNC: + ('POST', '/session/$sessionId/execute/async'), + Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'), + Command.GET_TITLE: ('GET', '/session/$sessionId/title'), + Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'), + Command.SCREENSHOT: ('GET', '/session/$sessionId/screenshot'), + Command.ELEMENT_SCREENSHOT: ('GET', '/session/$sessionId/element/$id/screenshot'), + Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'), + Command.FIND_ELEMENTS: ('POST', '/session/$sessionId/elements'), + Command.W3C_GET_ACTIVE_ELEMENT: ('GET', '/session/$sessionId/element/active'), + Command.GET_ACTIVE_ELEMENT: + ('POST', '/session/$sessionId/element/active'), + Command.FIND_CHILD_ELEMENT: + ('POST', '/session/$sessionId/element/$id/element'), + Command.FIND_CHILD_ELEMENTS: + ('POST', '/session/$sessionId/element/$id/elements'), + Command.CLICK_ELEMENT: ('POST', '/session/$sessionId/element/$id/click'), + Command.CLEAR_ELEMENT: ('POST', '/session/$sessionId/element/$id/clear'), + Command.SUBMIT_ELEMENT: ('POST', '/session/$sessionId/element/$id/submit'), + Command.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'), + Command.SEND_KEYS_TO_ELEMENT: + ('POST', '/session/$sessionId/element/$id/value'), + Command.SEND_KEYS_TO_ACTIVE_ELEMENT: + ('POST', '/session/$sessionId/keys'), + Command.UPLOAD_FILE: ('POST', "/session/$sessionId/file"), + Command.GET_ELEMENT_VALUE: + ('GET', '/session/$sessionId/element/$id/value'), + Command.GET_ELEMENT_TAG_NAME: + ('GET', '/session/$sessionId/element/$id/name'), + Command.IS_ELEMENT_SELECTED: + ('GET', '/session/$sessionId/element/$id/selected'), + Command.SET_ELEMENT_SELECTED: + ('POST', '/session/$sessionId/element/$id/selected'), + Command.IS_ELEMENT_ENABLED: + ('GET', '/session/$sessionId/element/$id/enabled'), + Command.IS_ELEMENT_DISPLAYED: + ('GET', '/session/$sessionId/element/$id/displayed'), + Command.GET_ELEMENT_LOCATION: + ('GET', '/session/$sessionId/element/$id/location'), + Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW: + ('GET', '/session/$sessionId/element/$id/location_in_view'), + Command.GET_ELEMENT_SIZE: + ('GET', '/session/$sessionId/element/$id/size'), + Command.GET_ELEMENT_RECT: + ('GET', '/session/$sessionId/element/$id/rect'), + Command.GET_ELEMENT_ATTRIBUTE: + ('GET', '/session/$sessionId/element/$id/attribute/$name'), + Command.GET_ELEMENT_PROPERTY: + ('GET', '/session/$sessionId/element/$id/property/$name'), + Command.GET_ALL_COOKIES: ('GET', '/session/$sessionId/cookie'), + Command.ADD_COOKIE: ('POST', '/session/$sessionId/cookie'), + Command.GET_COOKIE: ('GET', '/session/$sessionId/cookie/$name'), + Command.DELETE_ALL_COOKIES: + ('DELETE', '/session/$sessionId/cookie'), + Command.DELETE_COOKIE: + ('DELETE', '/session/$sessionId/cookie/$name'), + Command.SWITCH_TO_FRAME: ('POST', '/session/$sessionId/frame'), + Command.SWITCH_TO_PARENT_FRAME: ('POST', '/session/$sessionId/frame/parent'), + Command.SWITCH_TO_WINDOW: ('POST', '/session/$sessionId/window'), + Command.NEW_WINDOW: ('POST', '/session/$sessionId/window/new'), + Command.CLOSE: ('DELETE', '/session/$sessionId/window'), + Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY: + ('GET', '/session/$sessionId/element/$id/css/$propertyName'), + Command.IMPLICIT_WAIT: + ('POST', '/session/$sessionId/timeouts/implicit_wait'), + Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'), + Command.SET_SCRIPT_TIMEOUT: + ('POST', '/session/$sessionId/timeouts/async_script'), + Command.SET_TIMEOUTS: + ('POST', '/session/$sessionId/timeouts'), + Command.DISMISS_ALERT: + ('POST', '/session/$sessionId/dismiss_alert'), + Command.W3C_DISMISS_ALERT: + ('POST', '/session/$sessionId/alert/dismiss'), + Command.ACCEPT_ALERT: + ('POST', '/session/$sessionId/accept_alert'), + Command.W3C_ACCEPT_ALERT: + ('POST', '/session/$sessionId/alert/accept'), + Command.SET_ALERT_VALUE: + ('POST', '/session/$sessionId/alert_text'), + Command.W3C_SET_ALERT_VALUE: + ('POST', '/session/$sessionId/alert/text'), + Command.GET_ALERT_TEXT: + ('GET', '/session/$sessionId/alert_text'), + Command.W3C_GET_ALERT_TEXT: + ('GET', '/session/$sessionId/alert/text'), + Command.SET_ALERT_CREDENTIALS: + ('POST', '/session/$sessionId/alert/credentials'), + Command.CLICK: + ('POST', '/session/$sessionId/click'), + Command.W3C_ACTIONS: + ('POST', '/session/$sessionId/actions'), + Command.W3C_CLEAR_ACTIONS: + ('DELETE', '/session/$sessionId/actions'), + Command.DOUBLE_CLICK: + ('POST', '/session/$sessionId/doubleclick'), + Command.MOUSE_DOWN: + ('POST', '/session/$sessionId/buttondown'), + Command.MOUSE_UP: + ('POST', '/session/$sessionId/buttonup'), + Command.MOVE_TO: + ('POST', '/session/$sessionId/moveto'), + Command.GET_WINDOW_SIZE: + ('GET', '/session/$sessionId/window/$windowHandle/size'), + Command.SET_WINDOW_SIZE: + ('POST', '/session/$sessionId/window/$windowHandle/size'), + Command.GET_WINDOW_POSITION: + ('GET', '/session/$sessionId/window/$windowHandle/position'), + Command.SET_WINDOW_POSITION: + ('POST', '/session/$sessionId/window/$windowHandle/position'), + Command.SET_WINDOW_RECT: + ('POST', '/session/$sessionId/window/rect'), + Command.GET_WINDOW_RECT: + ('GET', '/session/$sessionId/window/rect'), + Command.MAXIMIZE_WINDOW: + ('POST', '/session/$sessionId/window/$windowHandle/maximize'), + Command.W3C_MAXIMIZE_WINDOW: + ('POST', '/session/$sessionId/window/maximize'), + Command.SET_SCREEN_ORIENTATION: + ('POST', '/session/$sessionId/orientation'), + Command.GET_SCREEN_ORIENTATION: + ('GET', '/session/$sessionId/orientation'), + Command.SINGLE_TAP: + ('POST', '/session/$sessionId/touch/click'), + Command.TOUCH_DOWN: + ('POST', '/session/$sessionId/touch/down'), + Command.TOUCH_UP: + ('POST', '/session/$sessionId/touch/up'), + Command.TOUCH_MOVE: + ('POST', '/session/$sessionId/touch/move'), + Command.TOUCH_SCROLL: + ('POST', '/session/$sessionId/touch/scroll'), + Command.DOUBLE_TAP: + ('POST', '/session/$sessionId/touch/doubleclick'), + Command.LONG_PRESS: + ('POST', '/session/$sessionId/touch/longclick'), + Command.FLICK: + ('POST', '/session/$sessionId/touch/flick'), + Command.EXECUTE_SQL: + ('POST', '/session/$sessionId/execute_sql'), + Command.GET_LOCATION: + ('GET', '/session/$sessionId/location'), + Command.SET_LOCATION: + ('POST', '/session/$sessionId/location'), + Command.GET_APP_CACHE: + ('GET', '/session/$sessionId/application_cache'), + Command.GET_APP_CACHE_STATUS: + ('GET', '/session/$sessionId/application_cache/status'), + Command.CLEAR_APP_CACHE: + ('DELETE', '/session/$sessionId/application_cache/clear'), + Command.GET_NETWORK_CONNECTION: + ('GET', '/session/$sessionId/network_connection'), + Command.SET_NETWORK_CONNECTION: + ('POST', '/session/$sessionId/network_connection'), + Command.GET_LOCAL_STORAGE_ITEM: + ('GET', '/session/$sessionId/local_storage/key/$key'), + Command.REMOVE_LOCAL_STORAGE_ITEM: + ('DELETE', '/session/$sessionId/local_storage/key/$key'), + Command.GET_LOCAL_STORAGE_KEYS: + ('GET', '/session/$sessionId/local_storage'), + Command.SET_LOCAL_STORAGE_ITEM: + ('POST', '/session/$sessionId/local_storage'), + Command.CLEAR_LOCAL_STORAGE: + ('DELETE', '/session/$sessionId/local_storage'), + Command.GET_LOCAL_STORAGE_SIZE: + ('GET', '/session/$sessionId/local_storage/size'), + Command.GET_SESSION_STORAGE_ITEM: + ('GET', '/session/$sessionId/session_storage/key/$key'), + Command.REMOVE_SESSION_STORAGE_ITEM: + ('DELETE', '/session/$sessionId/session_storage/key/$key'), + Command.GET_SESSION_STORAGE_KEYS: + ('GET', '/session/$sessionId/session_storage'), + Command.SET_SESSION_STORAGE_ITEM: + ('POST', '/session/$sessionId/session_storage'), + Command.CLEAR_SESSION_STORAGE: + ('DELETE', '/session/$sessionId/session_storage'), + Command.GET_SESSION_STORAGE_SIZE: + ('GET', '/session/$sessionId/session_storage/size'), + Command.GET_LOG: + ('POST', '/session/$sessionId/log'), + Command.GET_AVAILABLE_LOG_TYPES: + ('GET', '/session/$sessionId/log/types'), + Command.CURRENT_CONTEXT_HANDLE: + ('GET', '/session/$sessionId/context'), + Command.CONTEXT_HANDLES: + ('GET', '/session/$sessionId/contexts'), + Command.SWITCH_TO_CONTEXT: + ('POST', '/session/$sessionId/context'), + Command.FULLSCREEN_WINDOW: + ('POST', '/session/$sessionId/window/fullscreen'), + Command.MINIMIZE_WINDOW: + ('POST', '/session/$sessionId/window/minimize') + } + + def execute(self, command, params): + """ + Send a command to the remote server. + + Any path subtitutions required for the URL mapped to the command should be + included in the command parameters. + + :Args: + - command - A string specifying the command to execute. + - params - A dictionary of named parameters to send with the command as + its JSON payload. + """ + command_info = self._commands[command] + assert command_info is not None, 'Unrecognised command %s' % command + path = string.Template(command_info[1]).substitute(params) + if hasattr(self, 'w3c') and self.w3c and isinstance(params, dict) and 'sessionId' in params: + del params['sessionId'] + data = utils.dump_json(params) + url = '%s%s' % (self._url, path) + return self._request(command_info[0], url, body=data) + + def _request(self, method, url, body=None): + """ + Send an HTTP request to the remote server. + + :Args: + - method - A string for the HTTP method to send the request with. + - url - A string for the URL to send the request to. + - body - A string for request body. Ignored unless method is POST or PUT. + + :Returns: + A dictionary with the server's parsed JSON response. + """ + LOGGER.debug('%s %s %s' % (method, url, body)) + + parsed_url = parse.urlparse(url) + headers = self.get_remote_connection_headers(parsed_url, self.keep_alive) + resp = None + if body and method != 'POST' and method != 'PUT': + body = None + + if self.keep_alive: + resp = self._conn.request(method, url, body=body, headers=headers) + + statuscode = resp.status + else: + http = urllib3.PoolManager(timeout=self._timeout) + resp = http.request(method, url, body=body, headers=headers) + + statuscode = resp.status + if not hasattr(resp, 'getheader'): + if hasattr(resp.headers, 'getheader'): + resp.getheader = lambda x: resp.headers.getheader(x) + elif hasattr(resp.headers, 'get'): + resp.getheader = lambda x: resp.headers.get(x) + + data = resp.data.decode('UTF-8') + try: + if 300 <= statuscode < 304: + return self._request('GET', resp.getheader('location')) + if 399 < statuscode <= 500: + return {'status': statuscode, 'value': data} + content_type = [] + if resp.getheader('Content-Type') is not None: + content_type = resp.getheader('Content-Type').split(';') + if not any([x.startswith('image/png') for x in content_type]): + + try: + data = utils.load_json(data.strip()) + except ValueError: + if 199 < statuscode < 300: + status = ErrorCode.SUCCESS + else: + status = ErrorCode.UNKNOWN_ERROR + return {'status': status, 'value': data.strip()} + + # Some of the drivers incorrectly return a response + # with no 'value' field when they should return null. + if 'value' not in data: + data['value'] = None + return data + else: + data = {'status': 0, 'value': data} + return data + finally: + LOGGER.debug("Finished Request") + resp.close() + + def close(self): + """ + Clean up resources when finished with the remote_connection + """ + if hasattr(self, '_conn'): + self._conn.clear() diff --git a/selenium/webdriver/remote/switch_to.py b/selenium/webdriver/remote/switch_to.py new file mode 100644 index 00000000..a1aaacbb --- /dev/null +++ b/selenium/webdriver/remote/switch_to.py @@ -0,0 +1,160 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from .command import Command +from selenium.webdriver.common.alert import Alert +from selenium.webdriver.common.by import By +from selenium.common.exceptions import NoSuchElementException, NoSuchFrameException, NoSuchWindowException + +try: + basestring +except NameError: + basestring = str + + +class SwitchTo: + def __init__(self, driver): + self._driver = driver + + @property + def active_element(self): + """ + Returns the element with focus, or BODY if nothing has focus. + + :Usage: + :: + + element = driver.switch_to.active_element + """ + if self._driver.w3c: + return self._driver.execute(Command.W3C_GET_ACTIVE_ELEMENT)['value'] + else: + return self._driver.execute(Command.GET_ACTIVE_ELEMENT)['value'] + + @property + def alert(self): + """ + Switches focus to an alert on the page. + + :Usage: + :: + + alert = driver.switch_to.alert + """ + alert = Alert(self._driver) + alert.text + return alert + + def default_content(self): + """ + Switch focus to the default frame. + + :Usage: + :: + + driver.switch_to.default_content() + """ + self._driver.execute(Command.SWITCH_TO_FRAME, {'id': None}) + + def frame(self, frame_reference): + """ + Switches focus to the specified frame, by index, name, or webelement. + + :Args: + - frame_reference: The name of the window to switch to, an integer representing the index, + or a webelement that is an (i)frame to switch to. + + :Usage: + :: + + driver.switch_to.frame('frame_name') + driver.switch_to.frame(1) + driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0]) + """ + if isinstance(frame_reference, basestring) and self._driver.w3c: + try: + frame_reference = self._driver.find_element(By.ID, frame_reference) + except NoSuchElementException: + try: + frame_reference = self._driver.find_element(By.NAME, frame_reference) + except NoSuchElementException: + raise NoSuchFrameException(frame_reference) + + self._driver.execute(Command.SWITCH_TO_FRAME, {'id': frame_reference}) + + def new_window(self, type_hint=None): + """Switches to a new top-level browsing context. + + The type hint can be one of "tab" or "window". If not specified the + browser will automatically select it. + + :Usage: + :: + + driver.switch_to.new_window('tab') + """ + value = self._driver.execute(Command.NEW_WINDOW, {'type': type_hint})['value'] + self._w3c_window(value['handle']) + + def parent_frame(self): + """ + Switches focus to the parent context. If the current context is the top + level browsing context, the context remains unchanged. + + :Usage: + :: + + driver.switch_to.parent_frame() + """ + self._driver.execute(Command.SWITCH_TO_PARENT_FRAME) + + def window(self, window_name): + """ + Switches focus to the specified window. + + :Args: + - window_name: The name or window handle of the window to switch to. + + :Usage: + :: + + driver.switch_to.window('main') + """ + if self._driver.w3c: + self._w3c_window(window_name) + return + data = {'name': window_name} + self._driver.execute(Command.SWITCH_TO_WINDOW, data) + + def _w3c_window(self, window_name): + def send_handle(h): + self._driver.execute(Command.SWITCH_TO_WINDOW, {'handle': h}) + + try: + # Try using it as a handle first. + send_handle(window_name) + except NoSuchWindowException as e: + # Check every window to try to find the given window name. + original_handle = self._driver.current_window_handle + handles = self._driver.window_handles + for handle in handles: + send_handle(handle) + current_name = self._driver.execute_script('return window.name') + if window_name == current_name: + return + send_handle(original_handle) + raise e diff --git a/selenium/webdriver/remote/utils.py b/selenium/webdriver/remote/utils.py new file mode 100644 index 00000000..0599dc8f --- /dev/null +++ b/selenium/webdriver/remote/utils.py @@ -0,0 +1,90 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json +import logging +import os +import tempfile +import zipfile + + +LOGGER = logging.getLogger(__name__) + + +def format_json(json_struct): + return json.dumps(json_struct, indent=4) + + +def dump_json(json_struct): + return json.dumps(json_struct) + + +def load_json(s): + return json.loads(s) + + +def unzip_to_temp_dir(zip_file_name): + """Unzip zipfile to a temporary directory. + + The directory of the unzipped files is returned if success, + otherwise None is returned. """ + if not zip_file_name or not os.path.exists(zip_file_name): + return None + + zf = zipfile.ZipFile(zip_file_name) + + if zf.testzip() is not None: + return None + + # Unzip the files into a temporary directory + LOGGER.info("Extracting zipped file: %s" % zip_file_name) + tempdir = tempfile.mkdtemp() + + try: + # Create directories that don't exist + for zip_name in zf.namelist(): + # We have no knowledge on the os where the zipped file was + # created, so we restrict to zip files with paths without + # charactor "\" and "/". + name = (zip_name.replace("\\", os.path.sep). + replace("/", os.path.sep)) + dest = os.path.join(tempdir, name) + if (name.endswith(os.path.sep) and not os.path.exists(dest)): + os.mkdir(dest) + LOGGER.debug("Directory %s created." % dest) + + # Copy files + for zip_name in zf.namelist(): + # We have no knowledge on the os where the zipped file was + # created, so we restrict to zip files with paths without + # charactor "\" and "/". + name = (zip_name.replace("\\", os.path.sep). + replace("/", os.path.sep)) + dest = os.path.join(tempdir, name) + if not (name.endswith(os.path.sep)): + LOGGER.debug("Copying file %s......" % dest) + outfile = open(dest, 'wb') + outfile.write(zf.read(zip_name)) + outfile.close() + LOGGER.debug("File %s copied." % dest) + + LOGGER.info("Unzipped file can be found at %s" % tempdir) + return tempdir + + except IOError as err: + LOGGER.error("Error in extracting webdriver.xpi: %s" % err) + return None diff --git a/selenium/webdriver/remote/webdriver.py b/selenium/webdriver/remote/webdriver.py new file mode 100644 index 00000000..5cace703 --- /dev/null +++ b/selenium/webdriver/remote/webdriver.py @@ -0,0 +1,1333 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""The WebDriver implementation.""" + +import base64 +import copy +from contextlib import contextmanager +import warnings + +from .command import Command +from .webelement import WebElement +from .remote_connection import RemoteConnection +from .errorhandler import ErrorHandler +from .switch_to import SwitchTo +from .mobile import Mobile +from .file_detector import FileDetector, LocalFileDetector +from selenium.common.exceptions import (InvalidArgumentException, + WebDriverException, + NoSuchCookieException, + UnknownMethodException) +from selenium.webdriver.common.by import By +from selenium.webdriver.common.html5.application_cache import ApplicationCache + +try: + str = basestring +except NameError: + pass + + +_W3C_CAPABILITY_NAMES = frozenset([ + 'acceptInsecureCerts', + 'browserName', + 'browserVersion', + 'platformName', + 'pageLoadStrategy', + 'proxy', + 'setWindowRect', + 'timeouts', + 'unhandledPromptBehavior', + 'strictFileInteractability' +]) + +_OSS_W3C_CONVERSION = { + 'acceptSslCerts': 'acceptInsecureCerts', + 'version': 'browserVersion', + 'platform': 'platformName' +} + + +def _make_w3c_caps(caps): + """Makes a W3C alwaysMatch capabilities object. + + Filters out capability names that are not in the W3C spec. Spec-compliant + drivers will reject requests containing unknown capability names. + + Moves the Firefox profile, if present, from the old location to the new Firefox + options object. + + :Args: + - caps - A dictionary of capabilities requested by the caller. + """ + caps = copy.deepcopy(caps) + profile = caps.get('firefox_profile') + always_match = {} + if caps.get('proxy') and caps['proxy'].get('proxyType'): + caps['proxy']['proxyType'] = caps['proxy']['proxyType'].lower() + for k, v in caps.items(): + if v and k in _OSS_W3C_CONVERSION: + always_match[_OSS_W3C_CONVERSION[k]] = v.lower() if k == 'platform' else v + if k in _W3C_CAPABILITY_NAMES or ':' in k: + always_match[k] = v + if profile: + moz_opts = always_match.get('moz:firefoxOptions', {}) + # If it's already present, assume the caller did that intentionally. + if 'profile' not in moz_opts: + # Don't mutate the original capabilities. + new_opts = copy.deepcopy(moz_opts) + new_opts['profile'] = profile + always_match['moz:firefoxOptions'] = new_opts + return {"firstMatch": [{}], "alwaysMatch": always_match} + + +class WebDriver(object): + """ + Controls a browser by sending commands to a remote server. + This server is expected to be running the WebDriver wire protocol + as defined at + https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol + + :Attributes: + - session_id - String ID of the browser session started and controlled by this WebDriver. + - capabilities - Dictionary of effective capabilities of this browser session as returned + by the remote server. See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities + - command_executor - remote_connection.RemoteConnection object used to execute commands. + - error_handler - errorhandler.ErrorHandler object used to handle errors. + """ + + _web_element_cls = WebElement + + def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', + desired_capabilities=None, browser_profile=None, proxy=None, + keep_alive=True, file_detector=None, options=None): + """ + Create a new driver that will issue commands using the wire protocol. + + :Args: + - command_executor - Either a string representing URL of the remote server or a custom + remote_connection.RemoteConnection object. Defaults to 'http://127.0.0.1:4444/wd/hub'. + - desired_capabilities - A dictionary of capabilities to request when + starting the browser session. Required parameter. + - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. + Only used if Firefox is requested. Optional. + - proxy - A selenium.webdriver.common.proxy.Proxy object. The browser session will + be started with given proxy settings, if possible. Optional. + - keep_alive - Whether to configure remote_connection.RemoteConnection to use + HTTP keep-alive. Defaults to True. + - file_detector - Pass custom file detector object during instantiation. If None, + then default LocalFileDetector() will be used. + - options - instance of a driver options.Options class + """ + capabilities = {} + if options is not None: + capabilities = options.to_capabilities() + if desired_capabilities is not None: + if not isinstance(desired_capabilities, dict): + raise WebDriverException("Desired Capabilities must be a dictionary") + else: + capabilities.update(desired_capabilities) + self.command_executor = command_executor + if type(self.command_executor) is bytes or isinstance(self.command_executor, str): + self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive) + self._is_remote = True + self.session_id = None + self.capabilities = {} + self.error_handler = ErrorHandler() + self.start_client() + self.start_session(capabilities, browser_profile) + self._switch_to = SwitchTo(self) + self._mobile = Mobile(self) + self.file_detector = file_detector or LocalFileDetector() + + def __repr__(self): + return '<{0.__module__}.{0.__name__} (session="{1}")>'.format( + type(self), self.session_id) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.quit() + + @contextmanager + def file_detector_context(self, file_detector_class, *args, **kwargs): + """ + Overrides the current file detector (if necessary) in limited context. + Ensures the original file detector is set afterwards. + + Example: + + with webdriver.file_detector_context(UselessFileDetector): + someinput.send_keys('/etc/hosts') + + :Args: + - file_detector_class - Class of the desired file detector. If the class is different + from the current file_detector, then the class is instantiated with args and kwargs + and used as a file detector during the duration of the context manager. + - args - Optional arguments that get passed to the file detector class during + instantiation. + - kwargs - Keyword arguments, passed the same way as args. + """ + last_detector = None + if not isinstance(self.file_detector, file_detector_class): + last_detector = self.file_detector + self.file_detector = file_detector_class(*args, **kwargs) + try: + yield + finally: + if last_detector is not None: + self.file_detector = last_detector + + @property + def mobile(self): + return self._mobile + + @property + def name(self): + """Returns the name of the underlying browser for this instance. + + :Usage: + :: + + name = driver.name + """ + if 'browserName' in self.capabilities: + return self.capabilities['browserName'] + else: + raise KeyError('browserName not specified in session capabilities') + + def start_client(self): + """ + Called before starting a new session. This method may be overridden + to define custom startup behavior. + """ + pass + + def stop_client(self): + """ + Called after executing a quit command. This method may be overridden + to define custom shutdown behavior. + """ + pass + + def start_session(self, capabilities, browser_profile=None): + """ + Creates a new session with the desired capabilities. + + :Args: + - browser_name - The name of the browser to request. + - version - Which browser version to request. + - platform - Which platform to request the browser on. + - javascript_enabled - Whether the new session should support JavaScript. + - browser_profile - A selenium.webdriver.firefox.firefox_profile.FirefoxProfile object. Only used if Firefox is requested. + """ + if not isinstance(capabilities, dict): + raise InvalidArgumentException("Capabilities must be a dictionary") + if browser_profile: + if "moz:firefoxOptions" in capabilities: + capabilities["moz:firefoxOptions"]["profile"] = browser_profile.encoded + else: + capabilities.update({'firefox_profile': browser_profile.encoded}) + w3c_caps = _make_w3c_caps(capabilities) + parameters = {"capabilities": w3c_caps, + "desiredCapabilities": capabilities} + response = self.execute(Command.NEW_SESSION, parameters) + if 'sessionId' not in response: + response = response['value'] + self.session_id = response['sessionId'] + self.capabilities = response.get('value') + + # if capabilities is none we are probably speaking to + # a W3C endpoint + if self.capabilities is None: + self.capabilities = response.get('capabilities') + + # Double check to see if we have a W3C Compliant browser + self.w3c = response.get('status') is None + self.command_executor.w3c = self.w3c + + def _wrap_value(self, value): + if isinstance(value, dict): + converted = {} + for key, val in value.items(): + converted[key] = self._wrap_value(val) + return converted + elif isinstance(value, self._web_element_cls): + return {'ELEMENT': value.id, 'element-6066-11e4-a52e-4f735466cecf': value.id} + elif isinstance(value, list): + return list(self._wrap_value(item) for item in value) + else: + return value + + def create_web_element(self, element_id): + """Creates a web element with the specified `element_id`.""" + return self._web_element_cls(self, element_id, w3c=self.w3c) + + def _unwrap_value(self, value): + if isinstance(value, dict): + if 'ELEMENT' in value or 'element-6066-11e4-a52e-4f735466cecf' in value: + wrapped_id = value.get('ELEMENT', None) + if wrapped_id: + return self.create_web_element(value['ELEMENT']) + else: + return self.create_web_element(value['element-6066-11e4-a52e-4f735466cecf']) + else: + for key, val in value.items(): + value[key] = self._unwrap_value(val) + return value + elif isinstance(value, list): + return list(self._unwrap_value(item) for item in value) + else: + return value + + def execute(self, driver_command, params=None): + """ + Sends a command to be executed by a command.CommandExecutor. + + :Args: + - driver_command: The name of the command to execute as a string. + - params: A dictionary of named parameters to send with the command. + + :Returns: + The command's JSON response loaded into a dictionary object. + """ + if self.session_id is not None: + if not params: + params = {'sessionId': self.session_id} + elif 'sessionId' not in params: + params['sessionId'] = self.session_id + + params = self._wrap_value(params) + response = self.command_executor.execute(driver_command, params) + if response: + self.error_handler.check_response(response) + response['value'] = self._unwrap_value( + response.get('value', None)) + return response + # If the server doesn't send a response, assume the command was + # a success + return {'success': 0, 'value': None, 'sessionId': self.session_id} + + def get(self, url): + """ + Loads a web page in the current browser session. + """ + self.execute(Command.GET, {'url': url}) + + @property + def title(self): + """Returns the title of the current page. + + :Usage: + :: + + title = driver.title + """ + resp = self.execute(Command.GET_TITLE) + return resp['value'] if resp['value'] is not None else "" + + def find_element_by_id(self, id_): + """Finds an element by id. + + :Args: + - id\\_ - The id of the element to be found. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_id('foo') + """ + return self.find_element(by=By.ID, value=id_) + + def find_elements_by_id(self, id_): + """ + Finds multiple elements by id. + + :Args: + - id\\_ - The id of the elements to be found. + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_id('foo') + """ + return self.find_elements(by=By.ID, value=id_) + + def find_element_by_xpath(self, xpath): + """ + Finds an element by xpath. + + :Args: + - xpath - The xpath locator of the element to find. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_xpath('//div/td[1]') + """ + return self.find_element(by=By.XPATH, value=xpath) + + def find_elements_by_xpath(self, xpath): + """ + Finds multiple elements by xpath. + + :Args: + - xpath - The xpath locator of the elements to be found. + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_xpath("//div[contains(@class, 'foo')]") + """ + return self.find_elements(by=By.XPATH, value=xpath) + + def find_element_by_link_text(self, link_text): + """ + Finds an element by link text. + + :Args: + - link_text: The text of the element to be found. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_link_text('Sign In') + """ + return self.find_element(by=By.LINK_TEXT, value=link_text) + + def find_elements_by_link_text(self, text): + """ + Finds elements by link text. + + :Args: + - link_text: The text of the elements to be found. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_link_text('Sign In') + """ + return self.find_elements(by=By.LINK_TEXT, value=text) + + def find_element_by_partial_link_text(self, link_text): + """ + Finds an element by a partial match of its link text. + + :Args: + - link_text: The text of the element to partially match on. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_partial_link_text('Sign') + """ + return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_elements_by_partial_link_text(self, link_text): + """ + Finds elements by a partial match of their link text. + + :Args: + - link_text: The text of the element to partial match on. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_partial_link_text('Sign') + """ + return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_element_by_name(self, name): + """ + Finds an element by name. + + :Args: + - name: The name of the element to find. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_name('foo') + """ + return self.find_element(by=By.NAME, value=name) + + def find_elements_by_name(self, name): + """ + Finds elements by name. + + :Args: + - name: The name of the elements to find. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_name('foo') + """ + return self.find_elements(by=By.NAME, value=name) + + def find_element_by_tag_name(self, name): + """ + Finds an element by tag name. + + :Args: + - name - name of html tag (eg: h1, a, span) + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_tag_name('h1') + """ + return self.find_element(by=By.TAG_NAME, value=name) + + def find_elements_by_tag_name(self, name): + """ + Finds elements by tag name. + + :Args: + - name - name of html tag (eg: h1, a, span) + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_tag_name('h1') + """ + return self.find_elements(by=By.TAG_NAME, value=name) + + def find_element_by_class_name(self, name): + """ + Finds an element by class name. + + :Args: + - name: The class name of the element to find. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_class_name('foo') + """ + return self.find_element(by=By.CLASS_NAME, value=name) + + def find_elements_by_class_name(self, name): + """ + Finds elements by class name. + + :Args: + - name: The class name of the elements to find. + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_class_name('foo') + """ + return self.find_elements(by=By.CLASS_NAME, value=name) + + def find_element_by_css_selector(self, css_selector): + """ + Finds an element by css selector. + + :Args: + - css_selector - CSS selector string, ex: 'a.nav#home' + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = driver.find_element_by_css_selector('#foo') + """ + return self.find_element(by=By.CSS_SELECTOR, value=css_selector) + + def find_elements_by_css_selector(self, css_selector): + """ + Finds elements by css selector. + + :Args: + - css_selector - CSS selector string, ex: 'a.nav#home' + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = driver.find_elements_by_css_selector('.foo') + """ + return self.find_elements(by=By.CSS_SELECTOR, value=css_selector) + + def execute_script(self, script, *args): + """ + Synchronously Executes JavaScript in the current window/frame. + + :Args: + - script: The JavaScript to execute. + - \\*args: Any applicable arguments for your JavaScript. + + :Usage: + :: + + driver.execute_script('return document.title;') + """ + converted_args = list(args) + command = None + if self.w3c: + command = Command.W3C_EXECUTE_SCRIPT + else: + command = Command.EXECUTE_SCRIPT + + return self.execute(command, { + 'script': script, + 'args': converted_args})['value'] + + def execute_async_script(self, script, *args): + """ + Asynchronously Executes JavaScript in the current window/frame. + + :Args: + - script: The JavaScript to execute. + - \\*args: Any applicable arguments for your JavaScript. + + :Usage: + :: + + script = "var callback = arguments[arguments.length - 1]; " \\ + "window.setTimeout(function(){ callback('timeout') }, 3000);" + driver.execute_async_script(script) + """ + converted_args = list(args) + if self.w3c: + command = Command.W3C_EXECUTE_SCRIPT_ASYNC + else: + command = Command.EXECUTE_ASYNC_SCRIPT + + return self.execute(command, { + 'script': script, + 'args': converted_args})['value'] + + @property + def current_url(self): + """ + Gets the URL of the current page. + + :Usage: + :: + + driver.current_url + """ + return self.execute(Command.GET_CURRENT_URL)['value'] + + @property + def page_source(self): + """ + Gets the source of the current page. + + :Usage: + :: + + driver.page_source + """ + return self.execute(Command.GET_PAGE_SOURCE)['value'] + + def close(self): + """ + Closes the current window. + + :Usage: + :: + + driver.close() + """ + self.execute(Command.CLOSE) + + def quit(self): + """ + Quits the driver and closes every associated window. + + :Usage: + :: + + driver.quit() + """ + try: + self.execute(Command.QUIT) + finally: + self.stop_client() + self.command_executor.close() + + @property + def current_window_handle(self): + """ + Returns the handle of the current window. + + :Usage: + :: + + driver.current_window_handle + """ + if self.w3c: + return self.execute(Command.W3C_GET_CURRENT_WINDOW_HANDLE)['value'] + else: + return self.execute(Command.GET_CURRENT_WINDOW_HANDLE)['value'] + + @property + def window_handles(self): + """ + Returns the handles of all windows within the current session. + + :Usage: + :: + + driver.window_handles + """ + if self.w3c: + return self.execute(Command.W3C_GET_WINDOW_HANDLES)['value'] + else: + return self.execute(Command.GET_WINDOW_HANDLES)['value'] + + def maximize_window(self): + """ + Maximizes the current window that webdriver is using + """ + params = None + command = Command.W3C_MAXIMIZE_WINDOW + if not self.w3c: + command = Command.MAXIMIZE_WINDOW + params = {'windowHandle': 'current'} + self.execute(command, params) + + def fullscreen_window(self): + """ + Invokes the window manager-specific 'full screen' operation + """ + self.execute(Command.FULLSCREEN_WINDOW) + + def minimize_window(self): + """ + Invokes the window manager-specific 'minimize' operation + """ + self.execute(Command.MINIMIZE_WINDOW) + + @property + def switch_to(self): + """ + :Returns: + - SwitchTo: an object containing all options to switch focus into + + :Usage: + :: + + element = driver.switch_to.active_element + alert = driver.switch_to.alert + driver.switch_to.default_content() + driver.switch_to.frame('frame_name') + driver.switch_to.frame(1) + driver.switch_to.frame(driver.find_elements_by_tag_name("iframe")[0]) + driver.switch_to.parent_frame() + driver.switch_to.window('main') + """ + return self._switch_to + + # Navigation + def back(self): + """ + Goes one step backward in the browser history. + + :Usage: + :: + + driver.back() + """ + self.execute(Command.GO_BACK) + + def forward(self): + """ + Goes one step forward in the browser history. + + :Usage: + :: + + driver.forward() + """ + self.execute(Command.GO_FORWARD) + + def refresh(self): + """ + Refreshes the current page. + + :Usage: + :: + + driver.refresh() + """ + self.execute(Command.REFRESH) + + # Options + def get_cookies(self): + """ + Returns a set of dictionaries, corresponding to cookies visible in the current session. + + :Usage: + :: + + driver.get_cookies() + """ + return self.execute(Command.GET_ALL_COOKIES)['value'] + + def get_cookie(self, name): + """ + Get a single cookie by name. Returns the cookie if found, None if not. + + :Usage: + :: + + driver.get_cookie('my_cookie') + """ + if self.w3c: + try: + return self.execute(Command.GET_COOKIE, {'name': name})['value'] + except NoSuchCookieException: + return None + else: + cookies = self.get_cookies() + for cookie in cookies: + if cookie['name'] == name: + return cookie + return None + + def delete_cookie(self, name): + """ + Deletes a single cookie with the given name. + + :Usage: + :: + + driver.delete_cookie('my_cookie') + """ + self.execute(Command.DELETE_COOKIE, {'name': name}) + + def delete_all_cookies(self): + """ + Delete all cookies in the scope of the session. + + :Usage: + :: + + driver.delete_all_cookies() + """ + self.execute(Command.DELETE_ALL_COOKIES) + + def add_cookie(self, cookie_dict): + """ + Adds a cookie to your current session. + + :Args: + - cookie_dict: A dictionary object, with required keys - "name" and "value"; + optional keys - "path", "domain", "secure", "expiry" + + Usage: + driver.add_cookie({'name' : 'foo', 'value' : 'bar'}) + driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/'}) + driver.add_cookie({'name' : 'foo', 'value' : 'bar', 'path' : '/', 'secure':True}) + + """ + self.execute(Command.ADD_COOKIE, {'cookie': cookie_dict}) + + # Timeouts + def implicitly_wait(self, time_to_wait): + """ + Sets a sticky timeout to implicitly wait for an element to be found, + or a command to complete. This method only needs to be called one + time per session. To set the timeout for calls to + execute_async_script, see set_script_timeout. + + :Args: + - time_to_wait: Amount of time to wait (in seconds) + + :Usage: + :: + + driver.implicitly_wait(30) + """ + if self.w3c: + self.execute(Command.SET_TIMEOUTS, { + 'implicit': int(float(time_to_wait) * 1000)}) + else: + self.execute(Command.IMPLICIT_WAIT, { + 'ms': float(time_to_wait) * 1000}) + + def set_script_timeout(self, time_to_wait): + """ + Set the amount of time that the script should wait during an + execute_async_script call before throwing an error. + + :Args: + - time_to_wait: The amount of time to wait (in seconds) + + :Usage: + :: + + driver.set_script_timeout(30) + """ + if self.w3c: + self.execute(Command.SET_TIMEOUTS, { + 'script': int(float(time_to_wait) * 1000)}) + else: + self.execute(Command.SET_SCRIPT_TIMEOUT, { + 'ms': float(time_to_wait) * 1000}) + + def set_page_load_timeout(self, time_to_wait): + """ + Set the amount of time to wait for a page load to complete + before throwing an error. + + :Args: + - time_to_wait: The amount of time to wait + + :Usage: + :: + + driver.set_page_load_timeout(30) + """ + try: + self.execute(Command.SET_TIMEOUTS, { + 'pageLoad': int(float(time_to_wait) * 1000)}) + except WebDriverException: + self.execute(Command.SET_TIMEOUTS, { + 'ms': float(time_to_wait) * 1000, + 'type': 'page load'}) + + def find_element(self, by=By.ID, value=None): + """ + Find an element given a By strategy and locator. Prefer the find_element_by_* methods when + possible. + + :Usage: + :: + + element = driver.find_element(By.ID, 'foo') + + :rtype: WebElement + """ + if self.w3c: + if by == By.ID: + by = By.CSS_SELECTOR + value = '[id="%s"]' % value + elif by == By.TAG_NAME: + by = By.CSS_SELECTOR + elif by == By.CLASS_NAME: + by = By.CSS_SELECTOR + value = ".%s" % value + elif by == By.NAME: + by = By.CSS_SELECTOR + value = '[name="%s"]' % value + return self.execute(Command.FIND_ELEMENT, { + 'using': by, + 'value': value})['value'] + + def find_elements(self, by=By.ID, value=None): + """ + Find elements given a By strategy and locator. Prefer the find_elements_by_* methods when + possible. + + :Usage: + :: + + elements = driver.find_elements(By.CLASS_NAME, 'foo') + + :rtype: list of WebElement + """ + if self.w3c: + if by == By.ID: + by = By.CSS_SELECTOR + value = '[id="%s"]' % value + elif by == By.TAG_NAME: + by = By.CSS_SELECTOR + elif by == By.CLASS_NAME: + by = By.CSS_SELECTOR + value = ".%s" % value + elif by == By.NAME: + by = By.CSS_SELECTOR + value = '[name="%s"]' % value + + # Return empty list if driver returns null + # See https://github.com/SeleniumHQ/selenium/issues/4555 + return self.execute(Command.FIND_ELEMENTS, { + 'using': by, + 'value': value})['value'] or [] + + @property + def desired_capabilities(self): + """ + returns the drivers current desired capabilities being used + """ + return self.capabilities + + def get_screenshot_as_file(self, filename): + """ + Saves a screenshot of the current window to a PNG image file. Returns + False if there is any IOError, else returns True. Use full paths in + your filename. + + :Args: + - filename: The full path you wish to save your screenshot to. This + should end with a `.png` extension. + + :Usage: + :: + + driver.get_screenshot_as_file('/Screenshots/foo.png') + """ + if not filename.lower().endswith('.png'): + warnings.warn("name used for saved screenshot does not match file " + "type. It should end with a `.png` extension", UserWarning) + png = self.get_screenshot_as_png() + try: + with open(filename, 'wb') as f: + f.write(png) + except IOError: + return False + finally: + del png + return True + + def save_screenshot(self, filename): + """ + Saves a screenshot of the current window to a PNG image file. Returns + False if there is any IOError, else returns True. Use full paths in + your filename. + + :Args: + - filename: The full path you wish to save your screenshot to. This + should end with a `.png` extension. + + :Usage: + :: + + driver.save_screenshot('/Screenshots/foo.png') + """ + return self.get_screenshot_as_file(filename) + + def get_screenshot_as_png(self): + """ + Gets the screenshot of the current window as a binary data. + + :Usage: + :: + + driver.get_screenshot_as_png() + """ + return base64.b64decode(self.get_screenshot_as_base64().encode('ascii')) + + def get_screenshot_as_base64(self): + """ + Gets the screenshot of the current window as a base64 encoded string + which is useful in embedded images in HTML. + + :Usage: + :: + + driver.get_screenshot_as_base64() + """ + return self.execute(Command.SCREENSHOT)['value'] + + def set_window_size(self, width, height, windowHandle='current'): + """ + Sets the width and height of the current window. (window.resizeTo) + + :Args: + - width: the width in pixels to set the window to + - height: the height in pixels to set the window to + + :Usage: + :: + + driver.set_window_size(800,600) + """ + if self.w3c: + if windowHandle != 'current': + warnings.warn("Only 'current' window is supported for W3C compatibile browsers.") + self.set_window_rect(width=int(width), height=int(height)) + else: + self.execute(Command.SET_WINDOW_SIZE, { + 'width': int(width), + 'height': int(height), + 'windowHandle': windowHandle}) + + def get_window_size(self, windowHandle='current'): + """ + Gets the width and height of the current window. + + :Usage: + :: + + driver.get_window_size() + """ + command = Command.GET_WINDOW_SIZE + if self.w3c: + if windowHandle != 'current': + warnings.warn("Only 'current' window is supported for W3C compatibile browsers.") + size = self.get_window_rect() + else: + size = self.execute(command, {'windowHandle': windowHandle}) + + if size.get('value', None) is not None: + size = size['value'] + + return {k: size[k] for k in ('width', 'height')} + + def set_window_position(self, x, y, windowHandle='current'): + """ + Sets the x,y position of the current window. (window.moveTo) + + :Args: + - x: the x-coordinate in pixels to set the window position + - y: the y-coordinate in pixels to set the window position + + :Usage: + :: + + driver.set_window_position(0,0) + """ + if self.w3c: + if windowHandle != 'current': + warnings.warn("Only 'current' window is supported for W3C compatibile browsers.") + return self.set_window_rect(x=int(x), y=int(y)) + else: + self.execute(Command.SET_WINDOW_POSITION, + { + 'x': int(x), + 'y': int(y), + 'windowHandle': windowHandle + }) + + def get_window_position(self, windowHandle='current'): + """ + Gets the x,y position of the current window. + + :Usage: + :: + + driver.get_window_position() + """ + if self.w3c: + if windowHandle != 'current': + warnings.warn("Only 'current' window is supported for W3C compatibile browsers.") + position = self.get_window_rect() + else: + position = self.execute(Command.GET_WINDOW_POSITION, + {'windowHandle': windowHandle})['value'] + + return {k: position[k] for k in ('x', 'y')} + + def get_window_rect(self): + """ + Gets the x, y coordinates of the window as well as height and width of + the current window. + + :Usage: + :: + + driver.get_window_rect() + """ + return self.execute(Command.GET_WINDOW_RECT)['value'] + + def set_window_rect(self, x=None, y=None, width=None, height=None): + """ + Sets the x, y coordinates of the window as well as height and width of + the current window. This method is only supported for W3C compatible + browsers; other browsers should use `set_window_position` and + `set_window_size`. + + :Usage: + :: + + driver.set_window_rect(x=10, y=10) + driver.set_window_rect(width=100, height=200) + driver.set_window_rect(x=10, y=10, width=100, height=200) + """ + if not self.w3c: + raise UnknownMethodException("set_window_rect is only supported for W3C compatible browsers") + + if (x is None and y is None) and (height is None and width is None): + raise InvalidArgumentException("x and y or height and width need values") + + return self.execute(Command.SET_WINDOW_RECT, {"x": x, "y": y, + "width": width, + "height": height})['value'] + + @property + def file_detector(self): + return self._file_detector + + @file_detector.setter + def file_detector(self, detector): + """ + Set the file detector to be used when sending keyboard input. + By default, this is set to a file detector that does nothing. + + see FileDetector + see LocalFileDetector + see UselessFileDetector + + :Args: + - detector: The detector to use. Must not be None. + """ + if detector is None: + raise WebDriverException("You may not set a file detector that is null") + if not isinstance(detector, FileDetector): + raise WebDriverException("Detector has to be instance of FileDetector") + self._file_detector = detector + + @property + def orientation(self): + """ + Gets the current orientation of the device + + :Usage: + :: + + orientation = driver.orientation + """ + return self.execute(Command.GET_SCREEN_ORIENTATION)['value'] + + @orientation.setter + def orientation(self, value): + """ + Sets the current orientation of the device + + :Args: + - value: orientation to set it to. + + :Usage: + :: + + driver.orientation = 'landscape' + """ + allowed_values = ['LANDSCAPE', 'PORTRAIT'] + if value.upper() in allowed_values: + self.execute(Command.SET_SCREEN_ORIENTATION, {'orientation': value}) + else: + raise WebDriverException("You can only set the orientation to 'LANDSCAPE' and 'PORTRAIT'") + + @property + def application_cache(self): + """ Returns a ApplicationCache Object to interact with the browser app cache""" + return ApplicationCache(self) + + @property + def log_types(self): + """ + Gets a list of the available log types + + :Usage: + :: + + driver.log_types + """ + return self.execute(Command.GET_AVAILABLE_LOG_TYPES)['value'] + + def get_log(self, log_type): + """ + Gets the log for a given log type + + :Args: + - log_type: type of log that which will be returned + + :Usage: + :: + + driver.get_log('browser') + driver.get_log('driver') + driver.get_log('client') + driver.get_log('server') + """ + return self.execute(Command.GET_LOG, {'type': log_type})['value'] diff --git a/selenium/webdriver/remote/webelement.py b/selenium/webdriver/remote/webelement.py new file mode 100644 index 00000000..475b74aa --- /dev/null +++ b/selenium/webdriver/remote/webelement.py @@ -0,0 +1,751 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import hashlib +import os +import pkgutil +import warnings +import zipfile + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.common.utils import keys_to_typing +from .command import Command + +# Python 3 imports +try: + str = basestring +except NameError: + pass + +try: + from StringIO import StringIO as IOStream +except ImportError: # 3+ + from io import BytesIO as IOStream + +# not relying on __package__ here as it can be `None` in some situations (see #4558) +_pkg = '.'.join(__name__.split('.')[:-1]) +getAttribute_js = pkgutil.get_data(_pkg, 'getAttribute.js').decode('utf8') +isDisplayed_js = pkgutil.get_data(_pkg, 'isDisplayed.js').decode('utf8') + + +class WebElement(object): + """Represents a DOM element. + + Generally, all interesting operations that interact with a document will be + performed through this interface. + + All method calls will do a freshness check to ensure that the element + reference is still valid. This essentially determines whether or not the + element is still attached to the DOM. If this test fails, then an + ``StaleElementReferenceException`` is thrown, and all future calls to this + instance will fail.""" + + def __init__(self, parent, id_, w3c=False): + self._parent = parent + self._id = id_ + self._w3c = w3c + + def __repr__(self): + return '<{0.__module__}.{0.__name__} (session="{1}", element="{2}")>'.format( + type(self), self._parent.session_id, self._id) + + @property + def tag_name(self): + """This element's ``tagName`` property.""" + return self._execute(Command.GET_ELEMENT_TAG_NAME)['value'] + + @property + def text(self): + """The text of the element.""" + return self._execute(Command.GET_ELEMENT_TEXT)['value'] + + def click(self): + """Clicks the element.""" + self._execute(Command.CLICK_ELEMENT) + + def submit(self): + """Submits a form.""" + if self._w3c: + form = self.find_element(By.XPATH, "./ancestor-or-self::form") + self._parent.execute_script( + "var e = arguments[0].ownerDocument.createEvent('Event');" + "e.initEvent('submit', true, true);" + "if (arguments[0].dispatchEvent(e)) { arguments[0].submit() }", form) + else: + self._execute(Command.SUBMIT_ELEMENT) + + def clear(self): + """Clears the text if it's a text entry element.""" + self._execute(Command.CLEAR_ELEMENT) + + def get_property(self, name): + """ + Gets the given property of the element. + + :Args: + - name - Name of the property to retrieve. + + :Usage: + :: + + text_length = target_element.get_property("text_length") + """ + try: + return self._execute(Command.GET_ELEMENT_PROPERTY, {"name": name})["value"] + except WebDriverException: + # if we hit an end point that doesnt understand getElementProperty lets fake it + return self.parent.execute_script('return arguments[0][arguments[1]]', self, name) + + def get_attribute(self, name): + """Gets the given attribute or property of the element. + + This method will first try to return the value of a property with the + given name. If a property with that name doesn't exist, it returns the + value of the attribute with the same name. If there's no attribute with + that name, ``None`` is returned. + + Values which are considered truthy, that is equals "true" or "false", + are returned as booleans. All other non-``None`` values are returned + as strings. For attributes or properties which do not exist, ``None`` + is returned. + + :Args: + - name - Name of the attribute/property to retrieve. + + Example:: + + # Check if the "active" CSS class is applied to an element. + is_active = "active" in target_element.get_attribute("class") + + """ + + attributeValue = '' + if self._w3c: + attributeValue = self.parent.execute_script( + "return (%s).apply(null, arguments);" % getAttribute_js, + self, name) + else: + resp = self._execute(Command.GET_ELEMENT_ATTRIBUTE, {'name': name}) + attributeValue = resp.get('value') + if attributeValue is not None: + if name != 'value' and attributeValue.lower() in ('true', 'false'): + attributeValue = attributeValue.lower() + return attributeValue + + def is_selected(self): + """Returns whether the element is selected. + + Can be used to check if a checkbox or radio button is selected. + """ + return self._execute(Command.IS_ELEMENT_SELECTED)['value'] + + def is_enabled(self): + """Returns whether the element is enabled.""" + return self._execute(Command.IS_ELEMENT_ENABLED)['value'] + + def find_element_by_id(self, id_): + """Finds element within this element's children by ID. + + :Args: + - id\\_ - ID of child element to locate. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + foo_element = element.find_element_by_id('foo') + """ + return self.find_element(by=By.ID, value=id_) + + def find_elements_by_id(self, id_): + """Finds a list of elements within this element's children by ID. + Will return a list of webelements if found, or an empty list if not. + + :Args: + - id\\_ - Id of child element to find. + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_id('foo') + """ + return self.find_elements(by=By.ID, value=id_) + + def find_element_by_name(self, name): + """Finds element within this element's children by name. + + :Args: + - name - name property of the element to find. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_name('foo') + """ + return self.find_element(by=By.NAME, value=name) + + def find_elements_by_name(self, name): + """Finds a list of elements within this element's children by name. + + :Args: + - name - name property to search for. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_name('foo') + """ + return self.find_elements(by=By.NAME, value=name) + + def find_element_by_link_text(self, link_text): + """Finds element within this element's children by visible link text. + + :Args: + - link_text - Link text string to search for. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_link_text('Sign In') + """ + return self.find_element(by=By.LINK_TEXT, value=link_text) + + def find_elements_by_link_text(self, link_text): + """Finds a list of elements within this element's children by visible link text. + + :Args: + - link_text - Link text string to search for. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_link_text('Sign In') + """ + return self.find_elements(by=By.LINK_TEXT, value=link_text) + + def find_element_by_partial_link_text(self, link_text): + """Finds element within this element's children by partially visible link text. + + :Args: + - link_text: The text of the element to partially match on. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_partial_link_text('Sign') + """ + return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_elements_by_partial_link_text(self, link_text): + """Finds a list of elements within this element's children by link text. + + :Args: + - link_text: The text of the element to partial match on. + + :Returns: + - list of webelement - a list with elements if any was found. an + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_partial_link_text('Sign') + """ + return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_element_by_tag_name(self, name): + """Finds element within this element's children by tag name. + + :Args: + - name - name of html tag (eg: h1, a, span) + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_tag_name('h1') + """ + return self.find_element(by=By.TAG_NAME, value=name) + + def find_elements_by_tag_name(self, name): + """Finds a list of elements within this element's children by tag name. + + :Args: + - name - name of html tag (eg: h1, a, span) + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_tag_name('h1') + """ + return self.find_elements(by=By.TAG_NAME, value=name) + + def find_element_by_xpath(self, xpath): + """Finds element by xpath. + + :Args: + - xpath - xpath of element to locate. "//input[@class='myelement']" + + Note: The base path will be relative to this element's location. + + This will select the first link under this element. + + :: + + myelement.find_element_by_xpath(".//a") + + However, this will select the first link on the page. + + :: + + myelement.find_element_by_xpath("//a") + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_xpath('//div/td[1]') + """ + return self.find_element(by=By.XPATH, value=xpath) + + def find_elements_by_xpath(self, xpath): + """Finds elements within the element by xpath. + + :Args: + - xpath - xpath locator string. + + Note: The base path will be relative to this element's location. + + This will select all links under this element. + + :: + + myelement.find_elements_by_xpath(".//a") + + However, this will select all links in the page itself. + + :: + + myelement.find_elements_by_xpath("//a") + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_xpath("//div[contains(@class, 'foo')]") + + """ + return self.find_elements(by=By.XPATH, value=xpath) + + def find_element_by_class_name(self, name): + """Finds element within this element's children by class name. + + :Args: + - name: The class name of the element to find. + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_class_name('foo') + """ + return self.find_element(by=By.CLASS_NAME, value=name) + + def find_elements_by_class_name(self, name): + """Finds a list of elements within this element's children by class name. + + :Args: + - name: The class name of the elements to find. + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_class_name('foo') + """ + return self.find_elements(by=By.CLASS_NAME, value=name) + + def find_element_by_css_selector(self, css_selector): + """Finds element within this element's children by CSS selector. + + :Args: + - css_selector - CSS selector string, ex: 'a.nav#home' + + :Returns: + - WebElement - the element if it was found + + :Raises: + - NoSuchElementException - if the element wasn't found + + :Usage: + :: + + element = element.find_element_by_css_selector('#foo') + """ + return self.find_element(by=By.CSS_SELECTOR, value=css_selector) + + def find_elements_by_css_selector(self, css_selector): + """Finds a list of elements within this element's children by CSS selector. + + :Args: + - css_selector - CSS selector string, ex: 'a.nav#home' + + :Returns: + - list of WebElement - a list with elements if any was found. An + empty list if not + + :Usage: + :: + + elements = element.find_elements_by_css_selector('.foo') + """ + return self.find_elements(by=By.CSS_SELECTOR, value=css_selector) + + def send_keys(self, *value): + """Simulates typing into the element. + + :Args: + - value - A string for typing, or setting form fields. For setting + file inputs, this could be a local file path. + + Use this to send simple key events or to fill out form fields:: + + form_textfield = driver.find_element_by_name('username') + form_textfield.send_keys("admin") + + This can also be used to set file inputs. + + :: + + file_input = driver.find_element_by_name('profilePic') + file_input.send_keys("path/to/profilepic.gif") + # Generally it's better to wrap the file path in one of the methods + # in os.path to return the actual path to support cross OS testing. + # file_input.send_keys(os.path.abspath("path/to/profilepic.gif")) + + """ + # transfer file to another machine only if remote driver is used + # the same behaviour as for java binding + if self.parent._is_remote: + local_file = self.parent.file_detector.is_local_file(*value) + if local_file is not None: + value = self._upload(local_file) + + self._execute(Command.SEND_KEYS_TO_ELEMENT, + {'text': "".join(keys_to_typing(value)), + 'value': keys_to_typing(value)}) + + # RenderedWebElement Items + def is_displayed(self): + """Whether the element is visible to a user.""" + # Only go into this conditional for browsers that don't use the atom themselves + if self._w3c: + return self.parent.execute_script( + "return (%s).apply(null, arguments);" % isDisplayed_js, + self) + else: + return self._execute(Command.IS_ELEMENT_DISPLAYED)['value'] + + @property + def location_once_scrolled_into_view(self): + """THIS PROPERTY MAY CHANGE WITHOUT WARNING. Use this to discover + where on the screen an element is so that we can click it. This method + should cause the element to be scrolled into view. + + Returns the top lefthand corner location on the screen, or ``None`` if + the element is not visible. + + """ + if self._w3c: + old_loc = self._execute(Command.W3C_EXECUTE_SCRIPT, { + 'script': "arguments[0].scrollIntoView(true); return arguments[0].getBoundingClientRect()", + 'args': [self]})['value'] + return {"x": round(old_loc['x']), + "y": round(old_loc['y'])} + else: + return self._execute(Command.GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW)['value'] + + @property + def size(self): + """The size of the element.""" + size = {} + if self._w3c: + size = self._execute(Command.GET_ELEMENT_RECT)['value'] + else: + size = self._execute(Command.GET_ELEMENT_SIZE)['value'] + new_size = {"height": size["height"], + "width": size["width"]} + return new_size + + def value_of_css_property(self, property_name): + """The value of a CSS property.""" + return self._execute(Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY, { + 'propertyName': property_name})['value'] + + @property + def location(self): + """The location of the element in the renderable canvas.""" + if self._w3c: + old_loc = self._execute(Command.GET_ELEMENT_RECT)['value'] + else: + old_loc = self._execute(Command.GET_ELEMENT_LOCATION)['value'] + new_loc = {"x": round(old_loc['x']), + "y": round(old_loc['y'])} + return new_loc + + @property + def rect(self): + """A dictionary with the size and location of the element.""" + if self._w3c: + return self._execute(Command.GET_ELEMENT_RECT)['value'] + else: + rect = self.size.copy() + rect.update(self.location) + return rect + + @property + def screenshot_as_base64(self): + """ + Gets the screenshot of the current element as a base64 encoded string. + + :Usage: + :: + + img_b64 = element.screenshot_as_base64 + """ + return self._execute(Command.ELEMENT_SCREENSHOT)['value'] + + @property + def screenshot_as_png(self): + """ + Gets the screenshot of the current element as a binary data. + + :Usage: + :: + + element_png = element.screenshot_as_png + """ + return base64.b64decode(self.screenshot_as_base64.encode('ascii')) + + def screenshot(self, filename): + """ + Saves a screenshot of the current element to a PNG image file. Returns + False if there is any IOError, else returns True. Use full paths in + your filename. + + :Args: + - filename: The full path you wish to save your screenshot to. This + should end with a `.png` extension. + + :Usage: + :: + + element.screenshot('/Screenshots/foo.png') + """ + if not filename.lower().endswith('.png'): + warnings.warn("name used for saved screenshot does not match file " + "type. It should end with a `.png` extension", UserWarning) + png = self.screenshot_as_png + try: + with open(filename, 'wb') as f: + f.write(png) + except IOError: + return False + finally: + del png + return True + + @property + def parent(self): + """Internal reference to the WebDriver instance this element was found from.""" + return self._parent + + @property + def id(self): + """Internal ID used by selenium. + + This is mainly for internal use. Simple use cases such as checking if 2 + webelements refer to the same element, can be done using ``==``:: + + if element1 == element2: + print("These 2 are equal") + + """ + return self._id + + def __eq__(self, element): + return hasattr(element, 'id') and self._id == element.id + + def __ne__(self, element): + return not self.__eq__(element) + + # Private Methods + def _execute(self, command, params=None): + """Executes a command against the underlying HTML element. + + Args: + command: The name of the command to _execute as a string. + params: A dictionary of named parameters to send with the command. + + Returns: + The command's JSON response loaded into a dictionary object. + """ + if not params: + params = {} + params['id'] = self._id + return self._parent.execute(command, params) + + def find_element(self, by=By.ID, value=None): + """ + Find an element given a By strategy and locator. Prefer the find_element_by_* methods when + possible. + + :Usage: + :: + + element = element.find_element(By.ID, 'foo') + + :rtype: WebElement + """ + if self._w3c: + if by == By.ID: + by = By.CSS_SELECTOR + value = '[id="%s"]' % value + elif by == By.TAG_NAME: + by = By.CSS_SELECTOR + elif by == By.CLASS_NAME: + by = By.CSS_SELECTOR + value = ".%s" % value + elif by == By.NAME: + by = By.CSS_SELECTOR + value = '[name="%s"]' % value + + return self._execute(Command.FIND_CHILD_ELEMENT, + {"using": by, "value": value})['value'] + + def find_elements(self, by=By.ID, value=None): + """ + Find elements given a By strategy and locator. Prefer the find_elements_by_* methods when + possible. + + :Usage: + :: + + element = element.find_elements(By.CLASS_NAME, 'foo') + + :rtype: list of WebElement + """ + if self._w3c: + if by == By.ID: + by = By.CSS_SELECTOR + value = '[id="%s"]' % value + elif by == By.TAG_NAME: + by = By.CSS_SELECTOR + elif by == By.CLASS_NAME: + by = By.CSS_SELECTOR + value = ".%s" % value + elif by == By.NAME: + by = By.CSS_SELECTOR + value = '[name="%s"]' % value + + return self._execute(Command.FIND_CHILD_ELEMENTS, + {"using": by, "value": value})['value'] + + def __hash__(self): + return int(hashlib.md5(self._id.encode('utf-8')).hexdigest(), 16) + + def _upload(self, filename): + fp = IOStream() + zipped = zipfile.ZipFile(fp, 'w', zipfile.ZIP_DEFLATED) + zipped.write(filename, os.path.split(filename)[1]) + zipped.close() + content = base64.encodestring(fp.getvalue()) + if not isinstance(content, str): + content = content.decode('utf-8') + try: + return self._execute(Command.UPLOAD_FILE, {'file': content})['value'] + except WebDriverException as e: + if "Unrecognized command: POST" in e.__str__(): + return filename + elif "Command not found: POST " in e.__str__(): + return filename + elif '{"status":405,"value":["GET","HEAD","DELETE"]}' in e.__str__(): + return filename + else: + raise e diff --git a/selenium/webdriver/safari/__init__.py b/selenium/webdriver/safari/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/safari/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/safari/permissions.py b/selenium/webdriver/safari/permissions.py new file mode 100644 index 00000000..e95f2bc9 --- /dev/null +++ b/selenium/webdriver/safari/permissions.py @@ -0,0 +1,28 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +The Permission implementation. +""" + + +class Permission(object): + """ + Set of supported permissions. + """ + + GET_USER_MEDIA = "getUserMedia" diff --git a/selenium/webdriver/safari/remote_connection.py b/selenium/webdriver/safari/remote_connection.py new file mode 100644 index 00000000..ee3bdc66 --- /dev/null +++ b/selenium/webdriver/safari/remote_connection.py @@ -0,0 +1,27 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.remote.remote_connection import RemoteConnection + + +class SafariRemoteConnection(RemoteConnection): + def __init__(self, remote_server_addr, keep_alive=True): + RemoteConnection.__init__(self, remote_server_addr, keep_alive) + + self._commands["GET_PERMISSIONS"] = ('GET', '/session/$sessionId/apple/permissions') + self._commands["SET_PERMISSIONS"] = ('POST', '/session/$sessionId/apple/permissions') + self._commands["ATTACH_DEBUGGER"] = ('POST', '/session/$sessionId/apple/attach_debugger') diff --git a/selenium/webdriver/safari/service.py b/selenium/webdriver/safari/service.py new file mode 100644 index 00000000..bdbae56c --- /dev/null +++ b/selenium/webdriver/safari/service.py @@ -0,0 +1,64 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os +from selenium.webdriver.common import service, utils +from subprocess import PIPE + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the SafariDriver + """ + + def __init__(self, executable_path, port=0, quiet=False, service_args=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the SafariDriver + - port : Port the service is running on + - quiet : Suppress driver stdout and stderr + - service_args : List of args to pass to the safaridriver service """ + + if not os.path.exists(executable_path): + if "Safari Technology Preview" in executable_path: + message = "Safari Technology Preview does not seem to be installed. You can download it at https://developer.apple.com/safari/download/." + else: + message = "SafariDriver was not found; are you running Safari 10 or later? You can download Safari at https://developer.apple.com/safari/download/." + raise Exception(message) + + if port == 0: + port = utils.free_port() + + self.service_args = service_args or [] + + self.quiet = quiet + log = PIPE + if quiet: + log = open(os.devnull, 'w') + service.Service.__init__(self, executable_path, port, log) + + def command_line_args(self): + return ["-p", "%s" % self.port] + self.service_args + + @property + def service_url(self): + """ + Gets the url of the SafariDriver Service + """ + return "http://localhost:%d" % self.port diff --git a/selenium/webdriver/safari/webdriver.py b/selenium/webdriver/safari/webdriver.py new file mode 100644 index 00000000..547e60a1 --- /dev/null +++ b/selenium/webdriver/safari/webdriver.py @@ -0,0 +1,113 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from selenium.common.exceptions import WebDriverException + +try: + import http.client as http_client +except ImportError: + import httplib as http_client + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .service import Service +from .remote_connection import SafariRemoteConnection + + +class WebDriver(RemoteWebDriver): + """ + Controls the SafariDriver and allows you to drive the browser. + + """ + + def __init__(self, port=0, executable_path="/usr/bin/safaridriver", reuse_service=False, + desired_capabilities=DesiredCapabilities.SAFARI, quiet=False, + keep_alive=True, service_args=None): + """ + + Creates a new Safari driver instance and launches or finds a running safaridriver service. + + :Args: + - port - The port on which the safaridriver service should listen for new connections. If zero, a free port will be found. + - executable_path - Path to a custom safaridriver executable to be used. If absent, /usr/bin/safaridriver is used. + - reuse_service - If True, do not spawn a safaridriver instance; instead, connect to an already-running service that was launched externally. + - desired_capabilities: Dictionary object with desired capabilities (Can be used to provide various Safari switches). + - quiet - If True, the driver's stdout and stderr is suppressed. + - keep_alive - Whether to configure SafariRemoteConnection to use + HTTP keep-alive. Defaults to False. + - service_args : List of args to pass to the safaridriver service + """ + + self._reuse_service = reuse_service + self.service = Service(executable_path, port=port, quiet=quiet, service_args=service_args) + if not reuse_service: + self.service.start() + + executor = SafariRemoteConnection(remote_server_addr=self.service.service_url, + keep_alive=keep_alive) + + RemoteWebDriver.__init__( + self, + command_executor=executor, + desired_capabilities=desired_capabilities) + + self._is_remote = False + + def quit(self): + """ + Closes the browser and shuts down the SafariDriver executable + that is started when starting the SafariDriver + """ + try: + RemoteWebDriver.quit(self) + except http_client.BadStatusLine: + pass + finally: + if not self._reuse_service: + self.service.stop() + + # safaridriver extension commands. The canonical command support matrix is here: + # https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/WebDriverEndpointDoc/Commands/Commands.html + + # First available in Safari 11.1 and Safari Technology Preview 41. + def set_permission(self, permission, value): + if not isinstance(value, bool): + raise WebDriverException("Value of a session permission must be set to True or False.") + + payload = {} + payload[permission] = value + self.execute("SET_PERMISSIONS", {"permissions": payload}) + + # First available in Safari 11.1 and Safari Technology Preview 41. + def get_permission(self, permission): + payload = self.execute("GET_PERMISSIONS")["value"] + permissions = payload["permissions"] + if not permissions: + return None + + if permission not in permissions: + return None + + value = permissions[permission] + if not isinstance(value, bool): + return None + + return value + + # First available in Safari 11.1 and Safari Technology Preview 42. + def debug(self): + self.execute("ATTACH_DEBUGGER") + self.execute_script("debugger;") diff --git a/selenium/webdriver/support/__init__.py b/selenium/webdriver/support/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/support/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/support/abstract_event_listener.py b/selenium/webdriver/support/abstract_event_listener.py new file mode 100644 index 00000000..7dd166b7 --- /dev/null +++ b/selenium/webdriver/support/abstract_event_listener.py @@ -0,0 +1,79 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +class AbstractEventListener(object): + """ + Event listener must subclass and implement this fully or partially + """ + + def before_navigate_to(self, url, driver): + pass + + def after_navigate_to(self, url, driver): + pass + + def before_navigate_back(self, driver): + pass + + def after_navigate_back(self, driver): + pass + + def before_navigate_forward(self, driver): + pass + + def after_navigate_forward(self, driver): + pass + + def before_find(self, by, value, driver): + pass + + def after_find(self, by, value, driver): + pass + + def before_click(self, element, driver): + pass + + def after_click(self, element, driver): + pass + + def before_change_value_of(self, element, driver): + pass + + def after_change_value_of(self, element, driver): + pass + + def before_execute_script(self, script, driver): + pass + + def after_execute_script(self, script, driver): + pass + + def before_close(self, driver): + pass + + def after_close(self, driver): + pass + + def before_quit(self, driver): + pass + + def after_quit(self, driver): + pass + + def on_exception(self, exception, driver): + pass diff --git a/selenium/webdriver/support/color.py b/selenium/webdriver/support/color.py new file mode 100644 index 00000000..b94f7e51 --- /dev/null +++ b/selenium/webdriver/support/color.py @@ -0,0 +1,310 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +RGB_PATTERN = r"^\s*rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)\s*$" +RGB_PCT_PATTERN = r"^\s*rgb\(\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*\)\s*$" +RGBA_PATTERN = r"^\s*rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0\.\d+)\s*\)\s*$" +RGBA_PCT_PATTERN = r"^\s*rgba\(\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(\d{1,3}|\d{1,2}\.\d+)%\s*,\s*(0|1|0\.\d+)\s*\)\s*$" +HEX_PATTERN = r"#([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})" +HEX3_PATTERN = r"#([A-Fa-f0-9])([A-Fa-f0-9])([A-Fa-f0-9])" +HSL_PATTERN = r"^\s*hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)\s*$" +HSLA_PATTERN = r"^\s*hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(0|1|0\.\d+)\s*\)\s*$" + + +class Color(object): + """ + Color conversion support class + + Example: + + :: + + from selenium.webdriver.support.color import Color + + print(Color.from_string('#00ff33').rgba) + print(Color.from_string('rgb(1, 255, 3)').hex) + print(Color.from_string('blue').rgba) + """ + + @staticmethod + def from_string(str_): + import re + + class Matcher(object): + def __init__(self): + self.match_obj = None + + def match(self, pattern, str_): + self.match_obj = re.match(pattern, str_) + return self.match_obj + + @property + def groups(self): + return () if self.match_obj is None else self.match_obj.groups() + + m = Matcher() + + if m.match(RGB_PATTERN, str_): + return Color(*m.groups) + elif m.match(RGB_PCT_PATTERN, str_): + rgb = tuple([float(each) / 100 * 255 for each in m.groups]) + return Color(*rgb) + elif m.match(RGBA_PATTERN, str_): + return Color(*m.groups) + elif m.match(RGBA_PCT_PATTERN, str_): + rgba = tuple([float(each) / 100 * 255 for each in m.groups[:3]] + [m.groups[3]]) + return Color(*rgba) + elif m.match(HEX_PATTERN, str_): + rgb = tuple([int(each, 16) for each in m.groups]) + return Color(*rgb) + elif m.match(HEX3_PATTERN, str_): + rgb = tuple([int(each * 2, 16) for each in m.groups]) + return Color(*rgb) + elif m.match(HSL_PATTERN, str_) or m.match(HSLA_PATTERN, str_): + return Color._from_hsl(*m.groups) + elif str_.upper() in Colors.keys(): + return Colors[str_.upper()] + else: + raise ValueError("Could not convert %s into color" % str_) + + @staticmethod + def _from_hsl(h, s, l, a=1): + h = float(h) / 360 + s = float(s) / 100 + l = float(l) / 100 + + if s == 0: + r = l + g = r + b = r + else: + luminocity2 = l * (1 + s) if l < 0.5 else l + s - l * s + luminocity1 = 2 * l - luminocity2 + + def hue_to_rgb(lum1, lum2, hue): + if hue < 0.0: + hue += 1 + if hue > 1.0: + hue -= 1 + + if hue < 1.0 / 6.0: + return (lum1 + (lum2 - lum1) * 6.0 * hue) + elif hue < 1.0 / 2.0: + return lum2 + elif hue < 2.0 / 3.0: + return lum1 + (lum2 - lum1) * ((2.0 / 3.0) - hue) * 6.0 + else: + return lum1 + + r = hue_to_rgb(luminocity1, luminocity2, h + 1.0 / 3.0) + g = hue_to_rgb(luminocity1, luminocity2, h) + b = hue_to_rgb(luminocity1, luminocity2, h - 1.0 / 3.0) + + return Color(round(r * 255), round(g * 255), round(b * 255), a) + + def __init__(self, red, green, blue, alpha=1): + self.red = int(red) + self.green = int(green) + self.blue = int(blue) + self.alpha = "1" if float(alpha) == 1 else str(float(alpha) or 0) + + @property + def rgb(self): + return "rgb(%d, %d, %d)" % (self.red, self.green, self.blue) + + @property + def rgba(self): + return "rgba(%d, %d, %d, %s)" % (self.red, self.green, self.blue, self.alpha) + + @property + def hex(self): + return "#%02x%02x%02x" % (self.red, self.green, self.blue) + + def __eq__(self, other): + if isinstance(other, Color): + return self.rgba == other.rgba + return NotImplemented + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result + + def __hash__(self): + return hash((self.red, self.green, self.blue, self.alpha)) + + def __repr__(self): + return "Color(red=%d, green=%d, blue=%d, alpha=%s)" % (self.red, self.green, self.blue, self.alpha) + + def __str__(self): + return "Color: %s" % self.rgba + + +# Basic, extended and transparent colour keywords as defined by the W3C HTML4 spec +# See http://www.w3.org/TR/css3-color/#html4 +Colors = { + "TRANSPARENT": Color(0, 0, 0, 0), + "ALICEBLUE": Color(240, 248, 255), + "ANTIQUEWHITE": Color(250, 235, 215), + "AQUA": Color(0, 255, 255), + "AQUAMARINE": Color(127, 255, 212), + "AZURE": Color(240, 255, 255), + "BEIGE": Color(245, 245, 220), + "BISQUE": Color(255, 228, 196), + "BLACK": Color(0, 0, 0), + "BLANCHEDALMOND": Color(255, 235, 205), + "BLUE": Color(0, 0, 255), + "BLUEVIOLET": Color(138, 43, 226), + "BROWN": Color(165, 42, 42), + "BURLYWOOD": Color(222, 184, 135), + "CADETBLUE": Color(95, 158, 160), + "CHARTREUSE": Color(127, 255, 0), + "CHOCOLATE": Color(210, 105, 30), + "CORAL": Color(255, 127, 80), + "CORNFLOWERBLUE": Color(100, 149, 237), + "CORNSILK": Color(255, 248, 220), + "CRIMSON": Color(220, 20, 60), + "CYAN": Color(0, 255, 255), + "DARKBLUE": Color(0, 0, 139), + "DARKCYAN": Color(0, 139, 139), + "DARKGOLDENROD": Color(184, 134, 11), + "DARKGRAY": Color(169, 169, 169), + "DARKGREEN": Color(0, 100, 0), + "DARKGREY": Color(169, 169, 169), + "DARKKHAKI": Color(189, 183, 107), + "DARKMAGENTA": Color(139, 0, 139), + "DARKOLIVEGREEN": Color(85, 107, 47), + "DARKORANGE": Color(255, 140, 0), + "DARKORCHID": Color(153, 50, 204), + "DARKRED": Color(139, 0, 0), + "DARKSALMON": Color(233, 150, 122), + "DARKSEAGREEN": Color(143, 188, 143), + "DARKSLATEBLUE": Color(72, 61, 139), + "DARKSLATEGRAY": Color(47, 79, 79), + "DARKSLATEGREY": Color(47, 79, 79), + "DARKTURQUOISE": Color(0, 206, 209), + "DARKVIOLET": Color(148, 0, 211), + "DEEPPINK": Color(255, 20, 147), + "DEEPSKYBLUE": Color(0, 191, 255), + "DIMGRAY": Color(105, 105, 105), + "DIMGREY": Color(105, 105, 105), + "DODGERBLUE": Color(30, 144, 255), + "FIREBRICK": Color(178, 34, 34), + "FLORALWHITE": Color(255, 250, 240), + "FORESTGREEN": Color(34, 139, 34), + "FUCHSIA": Color(255, 0, 255), + "GAINSBORO": Color(220, 220, 220), + "GHOSTWHITE": Color(248, 248, 255), + "GOLD": Color(255, 215, 0), + "GOLDENROD": Color(218, 165, 32), + "GRAY": Color(128, 128, 128), + "GREY": Color(128, 128, 128), + "GREEN": Color(0, 128, 0), + "GREENYELLOW": Color(173, 255, 47), + "HONEYDEW": Color(240, 255, 240), + "HOTPINK": Color(255, 105, 180), + "INDIANRED": Color(205, 92, 92), + "INDIGO": Color(75, 0, 130), + "IVORY": Color(255, 255, 240), + "KHAKI": Color(240, 230, 140), + "LAVENDER": Color(230, 230, 250), + "LAVENDERBLUSH": Color(255, 240, 245), + "LAWNGREEN": Color(124, 252, 0), + "LEMONCHIFFON": Color(255, 250, 205), + "LIGHTBLUE": Color(173, 216, 230), + "LIGHTCORAL": Color(240, 128, 128), + "LIGHTCYAN": Color(224, 255, 255), + "LIGHTGOLDENRODYELLOW": Color(250, 250, 210), + "LIGHTGRAY": Color(211, 211, 211), + "LIGHTGREEN": Color(144, 238, 144), + "LIGHTGREY": Color(211, 211, 211), + "LIGHTPINK": Color(255, 182, 193), + "LIGHTSALMON": Color(255, 160, 122), + "LIGHTSEAGREEN": Color(32, 178, 170), + "LIGHTSKYBLUE": Color(135, 206, 250), + "LIGHTSLATEGRAY": Color(119, 136, 153), + "LIGHTSLATEGREY": Color(119, 136, 153), + "LIGHTSTEELBLUE": Color(176, 196, 222), + "LIGHTYELLOW": Color(255, 255, 224), + "LIME": Color(0, 255, 0), + "LIMEGREEN": Color(50, 205, 50), + "LINEN": Color(250, 240, 230), + "MAGENTA": Color(255, 0, 255), + "MAROON": Color(128, 0, 0), + "MEDIUMAQUAMARINE": Color(102, 205, 170), + "MEDIUMBLUE": Color(0, 0, 205), + "MEDIUMORCHID": Color(186, 85, 211), + "MEDIUMPURPLE": Color(147, 112, 219), + "MEDIUMSEAGREEN": Color(60, 179, 113), + "MEDIUMSLATEBLUE": Color(123, 104, 238), + "MEDIUMSPRINGGREEN": Color(0, 250, 154), + "MEDIUMTURQUOISE": Color(72, 209, 204), + "MEDIUMVIOLETRED": Color(199, 21, 133), + "MIDNIGHTBLUE": Color(25, 25, 112), + "MINTCREAM": Color(245, 255, 250), + "MISTYROSE": Color(255, 228, 225), + "MOCCASIN": Color(255, 228, 181), + "NAVAJOWHITE": Color(255, 222, 173), + "NAVY": Color(0, 0, 128), + "OLDLACE": Color(253, 245, 230), + "OLIVE": Color(128, 128, 0), + "OLIVEDRAB": Color(107, 142, 35), + "ORANGE": Color(255, 165, 0), + "ORANGERED": Color(255, 69, 0), + "ORCHID": Color(218, 112, 214), + "PALEGOLDENROD": Color(238, 232, 170), + "PALEGREEN": Color(152, 251, 152), + "PALETURQUOISE": Color(175, 238, 238), + "PALEVIOLETRED": Color(219, 112, 147), + "PAPAYAWHIP": Color(255, 239, 213), + "PEACHPUFF": Color(255, 218, 185), + "PERU": Color(205, 133, 63), + "PINK": Color(255, 192, 203), + "PLUM": Color(221, 160, 221), + "POWDERBLUE": Color(176, 224, 230), + "PURPLE": Color(128, 0, 128), + "REBECCAPURPLE": Color(128, 51, 153), + "RED": Color(255, 0, 0), + "ROSYBROWN": Color(188, 143, 143), + "ROYALBLUE": Color(65, 105, 225), + "SADDLEBROWN": Color(139, 69, 19), + "SALMON": Color(250, 128, 114), + "SANDYBROWN": Color(244, 164, 96), + "SEAGREEN": Color(46, 139, 87), + "SEASHELL": Color(255, 245, 238), + "SIENNA": Color(160, 82, 45), + "SILVER": Color(192, 192, 192), + "SKYBLUE": Color(135, 206, 235), + "SLATEBLUE": Color(106, 90, 205), + "SLATEGRAY": Color(112, 128, 144), + "SLATEGREY": Color(112, 128, 144), + "SNOW": Color(255, 250, 250), + "SPRINGGREEN": Color(0, 255, 127), + "STEELBLUE": Color(70, 130, 180), + "TAN": Color(210, 180, 140), + "TEAL": Color(0, 128, 128), + "THISTLE": Color(216, 191, 216), + "TOMATO": Color(255, 99, 71), + "TURQUOISE": Color(64, 224, 208), + "VIOLET": Color(238, 130, 238), + "WHEAT": Color(245, 222, 179), + "WHITE": Color(255, 255, 255), + "WHITESMOKE": Color(245, 245, 245), + "YELLOW": Color(255, 255, 0), + "YELLOWGREEN": Color(154, 205, 50) +} diff --git a/selenium/webdriver/support/event_firing_webdriver.py b/selenium/webdriver/support/event_firing_webdriver.py new file mode 100644 index 00000000..318dd85b --- /dev/null +++ b/selenium/webdriver/support/event_firing_webdriver.py @@ -0,0 +1,322 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement +from .abstract_event_listener import AbstractEventListener + + +def _wrap_elements(result, ef_driver): + if isinstance(result, WebElement): + return EventFiringWebElement(result, ef_driver) + elif isinstance(result, list): + return [_wrap_elements(item, ef_driver) for item in result] + else: + return result + + +class EventFiringWebDriver(object): + """ + A wrapper around an arbitrary WebDriver instance which supports firing events + """ + + def __init__(self, driver, event_listener): + """ + Creates a new instance of the EventFiringWebDriver + + :Args: + - driver : A WebDriver instance + - event_listener : Instance of a class that subclasses AbstractEventListener and implements it fully or partially + + Example: + + :: + + from selenium.webdriver import Firefox + from selenium.webdriver.support.events import EventFiringWebDriver, AbstractEventListener + + class MyListener(AbstractEventListener): + def before_navigate_to(self, url, driver): + print("Before navigate to %s" % url) + def after_navigate_to(self, url, driver): + print("After navigate to %s" % url) + + driver = Firefox() + ef_driver = EventFiringWebDriver(driver, MyListener()) + ef_driver.get("http://www.google.co.in/") + """ + if not isinstance(driver, WebDriver): + raise WebDriverException("A WebDriver instance must be supplied") + if not isinstance(event_listener, AbstractEventListener): + raise WebDriverException("Event listener must be a subclass of AbstractEventListener") + self._driver = driver + self._driver._wrap_value = self._wrap_value + self._listener = event_listener + + @property + def wrapped_driver(self): + """Returns the WebDriver instance wrapped by this EventsFiringWebDriver""" + return self._driver + + def get(self, url): + self._dispatch("navigate_to", (url, self._driver), "get", (url, )) + + def back(self): + self._dispatch("navigate_back", (self._driver,), "back", ()) + + def forward(self): + self._dispatch("navigate_forward", (self._driver,), "forward", ()) + + def execute_script(self, script, *args): + unwrapped_args = (script,) + self._unwrap_element_args(args) + return self._dispatch("execute_script", (script, self._driver), "execute_script", unwrapped_args) + + def execute_async_script(self, script, *args): + unwrapped_args = (script,) + self._unwrap_element_args(args) + return self._dispatch("execute_script", (script, self._driver), "execute_async_script", unwrapped_args) + + def close(self): + self._dispatch("close", (self._driver,), "close", ()) + + def quit(self): + self._dispatch("quit", (self._driver,), "quit", ()) + + def find_element(self, by=By.ID, value=None): + return self._dispatch("find", (by, value, self._driver), "find_element", (by, value)) + + def find_elements(self, by=By.ID, value=None): + return self._dispatch("find", (by, value, self._driver), "find_elements", (by, value)) + + def find_element_by_id(self, id_): + return self.find_element(by=By.ID, value=id_) + + def find_elements_by_id(self, id_): + return self.find_elements(by=By.ID, value=id_) + + def find_element_by_xpath(self, xpath): + return self.find_element(by=By.XPATH, value=xpath) + + def find_elements_by_xpath(self, xpath): + return self.find_elements(by=By.XPATH, value=xpath) + + def find_element_by_link_text(self, link_text): + return self.find_element(by=By.LINK_TEXT, value=link_text) + + def find_elements_by_link_text(self, text): + return self.find_elements(by=By.LINK_TEXT, value=text) + + def find_element_by_partial_link_text(self, link_text): + return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_elements_by_partial_link_text(self, link_text): + return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_element_by_name(self, name): + return self.find_element(by=By.NAME, value=name) + + def find_elements_by_name(self, name): + return self.find_elements(by=By.NAME, value=name) + + def find_element_by_tag_name(self, name): + return self.find_element(by=By.TAG_NAME, value=name) + + def find_elements_by_tag_name(self, name): + return self.find_elements(by=By.TAG_NAME, value=name) + + def find_element_by_class_name(self, name): + return self.find_element(by=By.CLASS_NAME, value=name) + + def find_elements_by_class_name(self, name): + return self.find_elements(by=By.CLASS_NAME, value=name) + + def find_element_by_css_selector(self, css_selector): + return self.find_element(by=By.CSS_SELECTOR, value=css_selector) + + def find_elements_by_css_selector(self, css_selector): + return self.find_elements(by=By.CSS_SELECTOR, value=css_selector) + + def _dispatch(self, l_call, l_args, d_call, d_args): + getattr(self._listener, "before_%s" % l_call)(*l_args) + try: + result = getattr(self._driver, d_call)(*d_args) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise e + getattr(self._listener, "after_%s" % l_call)(*l_args) + return _wrap_elements(result, self) + + def _unwrap_element_args(self, args): + if isinstance(args, EventFiringWebElement): + return args.wrapped_element + elif isinstance(args, tuple): + return tuple([self._unwrap_element_args(item) for item in args]) + elif isinstance(args, list): + return [self._unwrap_element_args(item) for item in args] + else: + return args + + def _wrap_value(self, value): + if isinstance(value, EventFiringWebElement): + return WebDriver._wrap_value(self._driver, value.wrapped_element) + return WebDriver._wrap_value(self._driver, value) + + def __setattr__(self, item, value): + if item.startswith("_") or not hasattr(self._driver, item): + object.__setattr__(self, item, value) + else: + try: + object.__setattr__(self._driver, item, value) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise e + + def __getattr__(self, name): + def _wrap(*args, **kwargs): + try: + result = attrib(*args, **kwargs) + return _wrap_elements(result, self) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise + + try: + attrib = getattr(self._driver, name) + return _wrap if callable(attrib) else attrib + except Exception as e: + self._listener.on_exception(e, self._driver) + raise + + +class EventFiringWebElement(object): + """" + A wrapper around WebElement instance which supports firing events + """ + + def __init__(self, webelement, ef_driver): + """ + Creates a new instance of the EventFiringWebElement + """ + self._webelement = webelement + self._ef_driver = ef_driver + self._driver = ef_driver.wrapped_driver + self._listener = ef_driver._listener + + @property + def wrapped_element(self): + """Returns the WebElement wrapped by this EventFiringWebElement instance""" + return self._webelement + + def click(self): + self._dispatch("click", (self._webelement, self._driver), "click", ()) + + def clear(self): + self._dispatch("change_value_of", (self._webelement, self._driver), "clear", ()) + + def send_keys(self, *value): + self._dispatch("change_value_of", (self._webelement, self._driver), "send_keys", value) + + def find_element(self, by=By.ID, value=None): + return self._dispatch("find", (by, value, self._driver), "find_element", (by, value)) + + def find_elements(self, by=By.ID, value=None): + return self._dispatch("find", (by, value, self._driver), "find_elements", (by, value)) + + def find_element_by_id(self, id_): + return self.find_element(by=By.ID, value=id_) + + def find_elements_by_id(self, id_): + return self.find_elements(by=By.ID, value=id_) + + def find_element_by_name(self, name): + return self.find_element(by=By.NAME, value=name) + + def find_elements_by_name(self, name): + return self.find_elements(by=By.NAME, value=name) + + def find_element_by_link_text(self, link_text): + return self.find_element(by=By.LINK_TEXT, value=link_text) + + def find_elements_by_link_text(self, link_text): + return self.find_elements(by=By.LINK_TEXT, value=link_text) + + def find_element_by_partial_link_text(self, link_text): + return self.find_element(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_elements_by_partial_link_text(self, link_text): + return self.find_elements(by=By.PARTIAL_LINK_TEXT, value=link_text) + + def find_element_by_tag_name(self, name): + return self.find_element(by=By.TAG_NAME, value=name) + + def find_elements_by_tag_name(self, name): + return self.find_elements(by=By.TAG_NAME, value=name) + + def find_element_by_xpath(self, xpath): + return self.find_element(by=By.XPATH, value=xpath) + + def find_elements_by_xpath(self, xpath): + return self.find_elements(by=By.XPATH, value=xpath) + + def find_element_by_class_name(self, name): + return self.find_element(by=By.CLASS_NAME, value=name) + + def find_elements_by_class_name(self, name): + return self.find_elements(by=By.CLASS_NAME, value=name) + + def find_element_by_css_selector(self, css_selector): + return self.find_element(by=By.CSS_SELECTOR, value=css_selector) + + def find_elements_by_css_selector(self, css_selector): + return self.find_elements(by=By.CSS_SELECTOR, value=css_selector) + + def _dispatch(self, l_call, l_args, d_call, d_args): + getattr(self._listener, "before_%s" % l_call)(*l_args) + try: + result = getattr(self._webelement, d_call)(*d_args) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise e + getattr(self._listener, "after_%s" % l_call)(*l_args) + return _wrap_elements(result, self._ef_driver) + + def __setattr__(self, item, value): + if item.startswith("_") or not hasattr(self._webelement, item): + object.__setattr__(self, item, value) + else: + try: + object.__setattr__(self._webelement, item, value) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise e + + def __getattr__(self, name): + def _wrap(*args, **kwargs): + try: + result = attrib(*args, **kwargs) + return _wrap_elements(result, self._ef_driver) + except Exception as e: + self._listener.on_exception(e, self._driver) + raise + + try: + attrib = getattr(self._webelement, name) + return _wrap if callable(attrib) else attrib + except Exception as e: + self._listener.on_exception(e, self._driver) + raise diff --git a/selenium/webdriver/support/events.py b/selenium/webdriver/support/events.py new file mode 100644 index 00000000..775d00a6 --- /dev/null +++ b/selenium/webdriver/support/events.py @@ -0,0 +1,19 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from .abstract_event_listener import AbstractEventListener # noqa +from .event_firing_webdriver import EventFiringWebDriver # noqa diff --git a/selenium/webdriver/support/expected_conditions.py b/selenium/webdriver/support/expected_conditions.py new file mode 100644 index 00000000..2a64920d --- /dev/null +++ b/selenium/webdriver/support/expected_conditions.py @@ -0,0 +1,422 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import NoSuchFrameException +from selenium.common.exceptions import StaleElementReferenceException +from selenium.common.exceptions import WebDriverException +from selenium.common.exceptions import NoAlertPresentException +from selenium.webdriver.remote.webdriver import WebElement + +""" + * Canned "Expected Conditions" which are generally useful within webdriver + * tests. +""" + + +class title_is(object): + """An expectation for checking the title of a page. + title is the expected title, which must be an exact match + returns True if the title matches, false otherwise.""" + def __init__(self, title): + self.title = title + + def __call__(self, driver): + return self.title == driver.title + + +class title_contains(object): + """ An expectation for checking that the title contains a case-sensitive + substring. title is the fragment of title expected + returns True when the title matches, False otherwise + """ + def __init__(self, title): + self.title = title + + def __call__(self, driver): + return self.title in driver.title + + +class presence_of_element_located(object): + """ An expectation for checking that an element is present on the DOM + of a page. This does not necessarily mean that the element is visible. + locator - used to find the element + returns the WebElement once it is located + """ + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + return _find_element(driver, self.locator) + + +class url_contains(object): + """ An expectation for checking that the current url contains a + case-sensitive substring. + url is the fragment of url expected, + returns True when the url matches, False otherwise + """ + def __init__(self, url): + self.url = url + + def __call__(self, driver): + return self.url in driver.current_url + + +class url_matches(object): + """An expectation for checking the current url. + pattern is the expected pattern, which must be an exact match + returns True if the url matches, false otherwise.""" + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, driver): + import re + match = re.search(self.pattern, driver.current_url) + + return match is not None + + +class url_to_be(object): + """An expectation for checking the current url. + url is the expected url, which must be an exact match + returns True if the url matches, false otherwise.""" + def __init__(self, url): + self.url = url + + def __call__(self, driver): + return self.url == driver.current_url + + +class url_changes(object): + """An expectation for checking the current url. + url is the expected url, which must not be an exact match + returns True if the url is different, false otherwise.""" + def __init__(self, url): + self.url = url + + def __call__(self, driver): + return self.url != driver.current_url + + +class visibility_of_element_located(object): + """ An expectation for checking that an element is present on the DOM of a + page and visible. Visibility means that the element is not only displayed + but also has a height and width that is greater than 0. + locator - used to find the element + returns the WebElement once it is located and visible + """ + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + try: + return _element_if_visible(_find_element(driver, self.locator)) + except StaleElementReferenceException: + return False + + +class visibility_of(object): + """ An expectation for checking that an element, known to be present on the + DOM of a page, is visible. Visibility means that the element is not only + displayed but also has a height and width that is greater than 0. + element is the WebElement + returns the (same) WebElement once it is visible + """ + def __init__(self, element): + self.element = element + + def __call__(self, ignored): + return _element_if_visible(self.element) + + +def _element_if_visible(element, visibility=True): + return element if element.is_displayed() == visibility else False + + +class presence_of_all_elements_located(object): + """ An expectation for checking that there is at least one element present + on a web page. + locator is used to find the element + returns the list of WebElements once they are located + """ + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + return _find_elements(driver, self.locator) + + +class visibility_of_any_elements_located(object): + """ An expectation for checking that there is at least one element visible + on a web page. + locator is used to find the element + returns the list of WebElements once they are located + """ + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + return [element for element in _find_elements(driver, self.locator) if _element_if_visible(element)] + + +class visibility_of_all_elements_located(object): + """ An expectation for checking that all elements are present on the DOM of a + page and visible. Visibility means that the elements are not only displayed + but also has a height and width that is greater than 0. + locator - used to find the elements + returns the list of WebElements once they are located and visible + """ + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + try: + elements = _find_elements(driver, self.locator) + for element in elements: + if _element_if_visible(element, visibility=False): + return False + return elements + except StaleElementReferenceException: + return False + + +class text_to_be_present_in_element(object): + """ An expectation for checking if the given text is present in the + specified element. + locator, text + """ + def __init__(self, locator, text_): + self.locator = locator + self.text = text_ + + def __call__(self, driver): + try: + element_text = _find_element(driver, self.locator).text + return self.text in element_text + except StaleElementReferenceException: + return False + + +class text_to_be_present_in_element_value(object): + """ + An expectation for checking if the given text is present in the element's + locator, text + """ + def __init__(self, locator, text_): + self.locator = locator + self.text = text_ + + def __call__(self, driver): + try: + element_text = _find_element(driver, + self.locator).get_attribute("value") + if element_text: + return self.text in element_text + else: + return False + except StaleElementReferenceException: + return False + + +class frame_to_be_available_and_switch_to_it(object): + """ An expectation for checking whether the given frame is available to + switch to. If the frame is available it switches the given driver to the + specified frame. + """ + def __init__(self, locator): + self.frame_locator = locator + + def __call__(self, driver): + try: + if isinstance(self.frame_locator, tuple): + driver.switch_to.frame(_find_element(driver, + self.frame_locator)) + else: + driver.switch_to.frame(self.frame_locator) + return True + except NoSuchFrameException: + return False + + +class invisibility_of_element_located(object): + """ An Expectation for checking that an element is either invisible or not + present on the DOM. + + locator used to find the element + """ + def __init__(self, locator): + self.target = locator + + def __call__(self, driver): + try: + target = self.target + if not isinstance(target, WebElement): + target = _find_element(driver, target) + return _element_if_visible(target, False) + except (NoSuchElementException, StaleElementReferenceException): + # In the case of NoSuchElement, returns true because the element is + # not present in DOM. The try block checks if the element is present + # but is invisible. + # In the case of StaleElementReference, returns true because stale + # element reference implies that element is no longer visible. + return True + + +class invisibility_of_element(invisibility_of_element_located): + """ An Expectation for checking that an element is either invisible or not + present on the DOM. + + element is either a locator (text) or an WebElement + """ + def __init(self, element): + self.target = element + + +class element_to_be_clickable(object): + """ An Expectation for checking an element is visible and enabled such that + you can click it.""" + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + element = visibility_of_element_located(self.locator)(driver) + if element and element.is_enabled(): + return element + else: + return False + + +class staleness_of(object): + """ Wait until an element is no longer attached to the DOM. + element is the element to wait for. + returns False if the element is still attached to the DOM, true otherwise. + """ + def __init__(self, element): + self.element = element + + def __call__(self, ignored): + try: + # Calling any method forces a staleness check + self.element.is_enabled() + return False + except StaleElementReferenceException: + return True + + +class element_to_be_selected(object): + """ An expectation for checking the selection is selected. + element is WebElement object + """ + def __init__(self, element): + self.element = element + + def __call__(self, ignored): + return self.element.is_selected() + + +class element_located_to_be_selected(object): + """An expectation for the element to be located is selected. + locator is a tuple of (by, path)""" + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + return _find_element(driver, self.locator).is_selected() + + +class element_selection_state_to_be(object): + """ An expectation for checking if the given element is selected. + element is WebElement object + is_selected is a Boolean." + """ + def __init__(self, element, is_selected): + self.element = element + self.is_selected = is_selected + + def __call__(self, ignored): + return self.element.is_selected() == self.is_selected + + +class element_located_selection_state_to_be(object): + """ An expectation to locate an element and check if the selection state + specified is in that state. + locator is a tuple of (by, path) + is_selected is a boolean + """ + def __init__(self, locator, is_selected): + self.locator = locator + self.is_selected = is_selected + + def __call__(self, driver): + try: + element = _find_element(driver, self.locator) + return element.is_selected() == self.is_selected + except StaleElementReferenceException: + return False + + +class number_of_windows_to_be(object): + """ An expectation for the number of windows to be a certain value.""" + + def __init__(self, num_windows): + self.num_windows = num_windows + + def __call__(self, driver): + return len(driver.window_handles) == self.num_windows + + +class new_window_is_opened(object): + """ An expectation that a new window will be opened and have the number of + windows handles increase""" + + def __init__(self, current_handles): + self.current_handles = current_handles + + def __call__(self, driver): + return len(driver.window_handles) > len(self.current_handles) + + +class alert_is_present(object): + """ Expect an alert to be present.""" + def __init__(self): + pass + + def __call__(self, driver): + try: + alert = driver.switch_to.alert + return alert + except NoAlertPresentException: + return False + + +def _find_element(driver, by): + """Looks up an element. Logs and re-raises ``WebDriverException`` + if thrown.""" + try: + return driver.find_element(*by) + except NoSuchElementException as e: + raise e + except WebDriverException as e: + raise e + + +def _find_elements(driver, by): + try: + return driver.find_elements(*by) + except WebDriverException as e: + raise e diff --git a/selenium/webdriver/support/select.py b/selenium/webdriver/support/select.py new file mode 100644 index 00000000..71675cad --- /dev/null +++ b/selenium/webdriver/support/select.py @@ -0,0 +1,241 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.by import By +from selenium.common.exceptions import NoSuchElementException, UnexpectedTagNameException + + +class Select(object): + + def __init__(self, webelement): + """ + Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not, + then an UnexpectedTagNameException is thrown. + + :Args: + - webelement - element SELECT element to wrap + + Example: + from selenium.webdriver.support.ui import Select \n + Select(driver.find_element_by_tag_name("select")).select_by_index(2) + """ + if webelement.tag_name.lower() != "select": + raise UnexpectedTagNameException( + "Select only works on <select> elements, not on <%s>" % + webelement.tag_name) + self._el = webelement + multi = self._el.get_attribute("multiple") + self.is_multiple = multi and multi != "false" + + @property + def options(self): + """Returns a list of all options belonging to this select tag""" + return self._el.find_elements(By.TAG_NAME, 'option') + + @property + def all_selected_options(self): + """Returns a list of all selected options belonging to this select tag""" + ret = [] + for opt in self.options: + if opt.is_selected(): + ret.append(opt) + return ret + + @property + def first_selected_option(self): + """The first selected option in this select tag (or the currently selected option in a + normal select)""" + for opt in self.options: + if opt.is_selected(): + return opt + raise NoSuchElementException("No options are selected") + + def select_by_value(self, value): + """Select all options that have a value matching the argument. That is, when given "foo" this + would select an option like: + + <option value="foo">Bar</option> + + :Args: + - value - The value to match against + + throws NoSuchElementException If there is no option with specified value in SELECT + """ + css = "option[value =%s]" % self._escapeString(value) + opts = self._el.find_elements(By.CSS_SELECTOR, css) + matched = False + for opt in opts: + self._setSelected(opt) + if not self.is_multiple: + return + matched = True + if not matched: + raise NoSuchElementException("Cannot locate option with value: %s" % value) + + def select_by_index(self, index): + """Select the option at the given index. This is done by examing the "index" attribute of an + element, and not merely by counting. + + :Args: + - index - The option at this index will be selected + + throws NoSuchElementException If there is no option with specified index in SELECT + """ + match = str(index) + for opt in self.options: + if opt.get_attribute("index") == match: + self._setSelected(opt) + return + raise NoSuchElementException("Could not locate element with index %d" % index) + + def select_by_visible_text(self, text): + """Select all options that display text matching the argument. That is, when given "Bar" this + would select an option like: + + <option value="foo">Bar</option> + + :Args: + - text - The visible text to match against + + throws NoSuchElementException If there is no option with specified text in SELECT + """ + xpath = ".//option[normalize-space(.) = %s]" % self._escapeString(text) + opts = self._el.find_elements(By.XPATH, xpath) + matched = False + for opt in opts: + self._setSelected(opt) + if not self.is_multiple: + return + matched = True + + if len(opts) == 0 and " " in text: + subStringWithoutSpace = self._get_longest_token(text) + if subStringWithoutSpace == "": + candidates = self.options + else: + xpath = ".//option[contains(.,%s)]" % self._escapeString(subStringWithoutSpace) + candidates = self._el.find_elements(By.XPATH, xpath) + for candidate in candidates: + if text == candidate.text: + self._setSelected(candidate) + if not self.is_multiple: + return + matched = True + + if not matched: + raise NoSuchElementException("Could not locate element with visible text: %s" % text) + + def deselect_all(self): + """Clear all selected entries. This is only valid when the SELECT supports multiple selections. + throws NotImplementedError If the SELECT does not support multiple selections + """ + if not self.is_multiple: + raise NotImplementedError("You may only deselect all options of a multi-select") + for opt in self.options: + self._unsetSelected(opt) + + def deselect_by_value(self, value): + """Deselect all options that have a value matching the argument. That is, when given "foo" this + would deselect an option like: + + <option value="foo">Bar</option> + + :Args: + - value - The value to match against + + throws NoSuchElementException If there is no option with specified value in SELECT + """ + if not self.is_multiple: + raise NotImplementedError("You may only deselect options of a multi-select") + matched = False + css = "option[value = %s]" % self._escapeString(value) + opts = self._el.find_elements(By.CSS_SELECTOR, css) + for opt in opts: + self._unsetSelected(opt) + matched = True + if not matched: + raise NoSuchElementException("Could not locate element with value: %s" % value) + + def deselect_by_index(self, index): + """Deselect the option at the given index. This is done by examing the "index" attribute of an + element, and not merely by counting. + + :Args: + - index - The option at this index will be deselected + + throws NoSuchElementException If there is no option with specified index in SELECT + """ + if not self.is_multiple: + raise NotImplementedError("You may only deselect options of a multi-select") + for opt in self.options: + if opt.get_attribute("index") == str(index): + self._unsetSelected(opt) + return + raise NoSuchElementException("Could not locate element with index %d" % index) + + def deselect_by_visible_text(self, text): + """Deselect all options that display text matching the argument. That is, when given "Bar" this + would deselect an option like: + + <option value="foo">Bar</option> + + :Args: + - text - The visible text to match against + """ + if not self.is_multiple: + raise NotImplementedError("You may only deselect options of a multi-select") + matched = False + xpath = ".//option[normalize-space(.) = %s]" % self._escapeString(text) + opts = self._el.find_elements(By.XPATH, xpath) + for opt in opts: + self._unsetSelected(opt) + matched = True + if not matched: + raise NoSuchElementException("Could not locate element with visible text: %s" % text) + + def _setSelected(self, option): + if not option.is_selected(): + option.click() + + def _unsetSelected(self, option): + if option.is_selected(): + option.click() + + def _escapeString(self, value): + if '"' in value and "'" in value: + substrings = value.split("\"") + result = ["concat("] + for substring in substrings: + result.append("\"%s\"" % substring) + result.append(", '\"', ") + result = result[0:-1] + if value.endswith('"'): + result.append(", '\"'") + return "".join(result) + ")" + + if '"' in value: + return "'%s'" % value + + return "\"%s\"" % value + + def _get_longest_token(self, value): + items = value.split(" ") + longest = "" + for item in items: + if len(item) > len(longest): + longest = item + return longest diff --git a/selenium/webdriver/support/ui.py b/selenium/webdriver/support/ui.py new file mode 100644 index 00000000..ee3b2b65 --- /dev/null +++ b/selenium/webdriver/support/ui.py @@ -0,0 +1,19 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from .select import Select # noqa +from .wait import WebDriverWait # noqa diff --git a/selenium/webdriver/support/wait.py b/selenium/webdriver/support/wait.py new file mode 100644 index 00000000..eb50ec6e --- /dev/null +++ b/selenium/webdriver/support/wait.py @@ -0,0 +1,109 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import time +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import TimeoutException + +POLL_FREQUENCY = 0.5 # How long to sleep inbetween calls to the method +IGNORED_EXCEPTIONS = (NoSuchElementException,) # exceptions ignored during calls to the method + + +class WebDriverWait(object): + def __init__(self, driver, timeout, poll_frequency=POLL_FREQUENCY, ignored_exceptions=None): + """Constructor, takes a WebDriver instance and timeout in seconds. + + :Args: + - driver - Instance of WebDriver (Ie, Firefox, Chrome or Remote) + - timeout - Number of seconds before timing out + - poll_frequency - sleep interval between calls + By default, it is 0.5 second. + - ignored_exceptions - iterable structure of exception classes ignored during calls. + By default, it contains NoSuchElementException only. + + Example: + from selenium.webdriver.support.ui import WebDriverWait \n + element = WebDriverWait(driver, 10).until(lambda x: x.find_element_by_id("someId")) \n + is_disappeared = WebDriverWait(driver, 30, 1, (ElementNotVisibleException)).\\ \n + until_not(lambda x: x.find_element_by_id("someId").is_displayed()) + """ + self._driver = driver + self._timeout = timeout + self._poll = poll_frequency + # avoid the divide by zero + if self._poll == 0: + self._poll = POLL_FREQUENCY + exceptions = list(IGNORED_EXCEPTIONS) + if ignored_exceptions is not None: + try: + exceptions.extend(iter(ignored_exceptions)) + except TypeError: # ignored_exceptions is not iterable + exceptions.append(ignored_exceptions) + self._ignored_exceptions = tuple(exceptions) + + def __repr__(self): + return '<{0.__module__}.{0.__name__} (session="{1}")>'.format( + type(self), self._driver.session_id) + + def until(self, method, message=''): + """Calls the method provided with the driver as an argument until the \ + return value does not evaluate to ``False``. + + :param method: callable(WebDriver) + :param message: optional message for :exc:`TimeoutException` + :returns: the result of the last call to `method` + :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs + """ + screen = None + stacktrace = None + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if value: + return value + except self._ignored_exceptions as exc: + screen = getattr(exc, 'screen', None) + stacktrace = getattr(exc, 'stacktrace', None) + time.sleep(self._poll) + if time.time() > end_time: + break + raise TimeoutException(message, screen, stacktrace) + + def until_not(self, method, message=''): + """Calls the method provided with the driver as an argument until the \ + return value evaluates to ``False``. + + :param method: callable(WebDriver) + :param message: optional message for :exc:`TimeoutException` + :returns: the result of the last call to `method`, or + ``True`` if `method` has raised one of the ignored exceptions + :raises: :exc:`selenium.common.exceptions.TimeoutException` if timeout occurs + """ + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if not value: + return value + except self._ignored_exceptions: + return True + time.sleep(self._poll) + if time.time() > end_time: + break + raise TimeoutException(message) diff --git a/selenium/webdriver/webkitgtk/__init__.py b/selenium/webdriver/webkitgtk/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/selenium/webdriver/webkitgtk/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/selenium/webdriver/webkitgtk/options.py b/selenium/webdriver/webkitgtk/options.py new file mode 100644 index 00000000..ad84c468 --- /dev/null +++ b/selenium/webdriver/webkitgtk/options.py @@ -0,0 +1,84 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.common.options import ArgOptions + + +class Options(ArgOptions): + KEY = 'webkitgtk:browserOptions' + + def __init__(self): + super(Options, self).__init__() + self._binary_location = '' + self._overlay_scrollbars_enabled = True + + @property + def binary_location(self): + """ + :Returns: The location of the browser binary otherwise an empty string + """ + return self._binary_location + + @binary_location.setter + def binary_location(self, value): + """ + Allows you to set the browser binary to launch + + :Args: + - value : path to the browser binary + """ + self._binary_location = value + + @property + def overlay_scrollbars_enabled(self): + """ + :Returns: Whether overlay scrollbars should be enabled + """ + return self._overlay_scrollbars_enabled + + @overlay_scrollbars_enabled.setter + def overlay_scrollbars_enabled(self, value): + """ + Allows you to enable or disable overlay scrollbars + + :Args: + - value : True or False + """ + self._overlay_scrollbars_enabled = value + + def to_capabilities(self): + """ + Creates a capabilities with all the options that have been set and + returns a dictionary with everything + """ + caps = self._caps + + browser_options = {} + if self.binary_location: + browser_options["binary"] = self.binary_location + if self.arguments: + browser_options["args"] = self.arguments + browser_options["useOverlayScrollbars"] = self.overlay_scrollbars_enabled + + caps[Options.KEY] = browser_options + + return caps + + @property + def default_capabilities(self): + return DesiredCapabilities.WEBKITGTK.copy() diff --git a/selenium/webdriver/webkitgtk/service.py b/selenium/webdriver/webkitgtk/service.py new file mode 100644 index 00000000..26dad741 --- /dev/null +++ b/selenium/webdriver/webkitgtk/service.py @@ -0,0 +1,42 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common import service + + +class Service(service.Service): + """ + Object that manages the starting and stopping of the WebKitGTKDriver + """ + + def __init__(self, executable_path, port=0, log_path=None): + """ + Creates a new instance of the Service + + :Args: + - executable_path : Path to the WebKitGTKDriver + - port : Port the service is running on + - log_path : Path for the WebKitGTKDriver service to log to + """ + log_file = open(log_path, "wb") if log_path is not None and log_path != "" else None + service.Service.__init__(self, executable_path, port, log_file) + + def command_line_args(self): + return ["-p", "%d" % self.port] + + def send_remote_shutdown_command(self): + pass diff --git a/selenium/webdriver/webkitgtk/webdriver.py b/selenium/webdriver/webkitgtk/webdriver.py new file mode 100644 index 00000000..22a257cb --- /dev/null +++ b/selenium/webdriver/webkitgtk/webdriver.py @@ -0,0 +1,78 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +try: + import http.client as http_client +except ImportError: + import httplib as http_client + +from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver +from .service import Service +from .options import Options + + +class WebDriver(RemoteWebDriver): + """ + Controls the WebKitGTKDriver and allows you to drive the browser. + """ + + def __init__(self, executable_path="WebKitWebDriver", port=0, options=None, + desired_capabilities=None, + service_log_path=None, keep_alive=False): + """ + Creates a new instance of the WebKitGTK driver. + + Starts the service and then creates new instance of WebKitGTK Driver. + + :Args: + - executable_path : path to the executable. If the default is used it assumes the executable is in the $PATH. + - port : port you would like the service to run, if left as 0, a free port will be found. + - options : an instance of WebKitGTKOptions + - desired_capabilities : Dictionary object with desired capabilities + - service_log_path : Path to write service stdout and stderr output. + - keep_alive : Whether to configure RemoteConnection to use HTTP keep-alive. + """ + if options is None: + if desired_capabilities is None: + desired_capabilities = Options().to_capabilities() + else: + capabilities = options.to_capabilities() + if desired_capabilities is not None: + capabilities.update(desired_capabilities) + desired_capabilities = capabilities + + self.service = Service(executable_path, port=port, log_path=service_log_path) + self.service.start() + + RemoteWebDriver.__init__( + self, + command_executor=self.service.service_url, + desired_capabilities=desired_capabilities, + keep_alive=keep_alive) + self._is_remote = False + + def quit(self): + """ + Closes the browser and shuts down the WebKitGTKDriver executable + that is started when starting the WebKitGTKDriver + """ + try: + RemoteWebDriver.quit(self) + except http_client.BadStatusLine: + pass + finally: + self.service.stop() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..1385258a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[bdist_wheel] +universal = 1 + +[flake8] +exclude = .tox,docs/source/conf.py +ignore = E501 + +[tool:pytest] +addopts = -r=a +python_files = test_*.py *_tests.py +testpaths = test diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..12a3850e --- /dev/null +++ b/setup.py @@ -0,0 +1,73 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from distutils.command.install import INSTALL_SCHEMES +from os.path import dirname, join, abspath +from setuptools import setup +from setuptools.command.install import install + + +for scheme in INSTALL_SCHEMES.values(): + scheme['data'] = scheme['purelib'] + +setup_args = { + 'cmdclass': {'install': install}, + 'name': 'selenium', + 'version': "4.0.0a1", + 'license': 'Apache 2.0', + 'description': 'Python bindings for Selenium', + 'long_description': open(join(abspath(dirname(__file__)), "README.rst")).read(), + 'url': 'https://github.com/SeleniumHQ/selenium/', + 'classifiers': ['Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: Libraries', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6'], + 'package_dir': { + 'selenium': 'selenium', + 'selenium.common': 'selenium/common', + 'selenium.webdriver': 'selenium/webdriver', + }, + 'packages': ['selenium', + 'selenium.common', + 'selenium.webdriver', + 'selenium.webdriver.android', + 'selenium.webdriver.chrome', + 'selenium.webdriver.common', + 'selenium.webdriver.common.html5', + 'selenium.webdriver.support', + 'selenium.webdriver.firefox', + 'selenium.webdriver.ie', + 'selenium.webdriver.edge', + 'selenium.webdriver.opera', + 'selenium.webdriver.phantomjs', + 'selenium.webdriver.remote', + 'selenium.webdriver.support', ], + 'include_package_data': True, + 'install_requires': ['urllib3'], + 'zip_safe': False +} + +setup(**setup_args) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/run_pytest.py b/test/run_pytest.py new file mode 100644 index 00000000..f6557514 --- /dev/null +++ b/test/run_pytest.py @@ -0,0 +1,2 @@ +import pytest +raise SystemExit(pytest.main()) \ No newline at end of file diff --git a/test/selenium/__init__.py b/test/selenium/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/__init__.py b/test/selenium/webdriver/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/chrome/__init__.py b/test/selenium/webdriver/chrome/__init__.py new file mode 100755 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/chrome/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py b/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py new file mode 100644 index 00000000..e6bd5f26 --- /dev/null +++ b/test/selenium/webdriver/chrome/chrome_execute_cdp_cmd_tests.py @@ -0,0 +1,25 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Chrome + + +def test_execute_cdp_cmd(): + driver = Chrome() + version_info = driver.execute_cdp_cmd('Browser.getVersion', {}) + assert isinstance(version_info, dict) + assert 'userAgent' in version_info diff --git a/test/selenium/webdriver/chrome/chrome_launcher_tests.py b/test/selenium/webdriver/chrome/chrome_launcher_tests.py new file mode 100755 index 00000000..b9470d1f --- /dev/null +++ b/test/selenium/webdriver/chrome/chrome_launcher_tests.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Chrome +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +def test_launch_and_close_browser(): + driver = Chrome() + driver.quit() + + +def test_we_can_launch_multiple_chrome_instances(): + driver1 = Chrome() + driver2 = Chrome() + driver3 = Chrome() + driver1.quit() + driver2.quit() + driver3.quit() + + +def test_launch_chrome_do_not_affect_default_capabilities(): + expected = DesiredCapabilities.CHROME.copy() + driver = Chrome() + actual = DesiredCapabilities.CHROME.copy() + driver.quit() + assert actual == expected diff --git a/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py b/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py new file mode 100755 index 00000000..342bc76b --- /dev/null +++ b/test/selenium/webdriver/chrome/chrome_network_emulation_tests.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Chrome + + +def test_network_conditions_emulation(): + driver = Chrome() + driver.set_network_conditions( + offline=False, + latency=56, # additional latency (ms) + throughput=789) + conditions = driver.get_network_conditions() + assert conditions['offline'] is False + assert conditions['latency'] == 56 + assert conditions['download_throughput'] == 789 + assert conditions['upload_throughput'] == 789 diff --git a/test/selenium/webdriver/common/__init__.py b/test/selenium/webdriver/common/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/common/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/common/alerts_tests.py b/test/selenium/webdriver/common/alerts_tests.py new file mode 100644 index 00000000..7ed1a12b --- /dev/null +++ b/test/selenium/webdriver/common/alerts_tests.py @@ -0,0 +1,306 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import sys + +import pytest + +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from selenium.common.exceptions import ( + InvalidElementStateException, + NoAlertPresentException, + UnexpectedAlertPresentException) + + +@pytest.fixture(autouse=True) +def close_alert(driver): + yield + try: + driver.switch_to.alert.dismiss() + except Exception: + pass + + +def testShouldBeAbleToOverrideTheWindowAlertMethod(driver, pages): + pages.load("alerts.html") + driver.execute_script( + "window.alert = function(msg) { document.getElementById('text').innerHTML = msg; }") + driver.find_element(by=By.ID, value="alert").click() + try: + assert driver.find_element_by_id('text').text == "cheese" + except Exception as e: + # if we're here, likely the alert is displayed + # not dismissing it will affect other tests + try: + _waitForAlert(driver).dismiss() + except Exception: + pass + raise e + + +def testShouldAllowUsersToAcceptAnAlertManually(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="alert").click() + alert = _waitForAlert(driver) + alert.accept() + # If we can perform any action, we're good to go + assert "Testing Alerts" == driver.title + + +def testShouldAllowUsersToAcceptAnAlertWithNoTextManually(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "empty-alert").click() + alert = _waitForAlert(driver) + alert.accept() + + # If we can perform any action, we're good to go + assert "Testing Alerts" == driver.title + + +def testShouldGetTextOfAlertOpenedInSetTimeout(driver, pages): + pages.load("alerts.html") + driver.find_element_by_id("slow-alert").click() + + # DO NOT WAIT OR SLEEP HERE + # This is a regression test for a bug where only the first switchTo call would throw, + # and only if it happens before the alert actually loads. + + alert = _waitForAlert(driver) + try: + assert "Slow" == alert.text + finally: + alert.accept() + + +def testShouldAllowUsersToDismissAnAlertManually(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="alert").click() + alert = _waitForAlert(driver) + alert.dismiss() + # If we can perform any action, we're good to go + assert "Testing Alerts" == driver.title + + +def testShouldAllowAUserToAcceptAPrompt(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="prompt").click() + alert = _waitForAlert(driver) + alert.accept() + + # If we can perform any action, we're good to go + assert "Testing Alerts" == driver.title + + +def testShouldAllowAUserToDismissAPrompt(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="prompt").click() + alert = _waitForAlert(driver) + alert.dismiss() + + # If we can perform any action, we're good to go + assert "Testing Alerts" == driver.title + + +def testShouldAllowAUserToSetTheValueOfAPrompt(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="prompt").click() + alert = _waitForAlert(driver) + alert.send_keys("cheese") + alert.accept() + + result = driver.find_element(by=By.ID, value="text").text + assert "cheese" == result + + +def testSettingTheValueOfAnAlertThrows(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "alert").click() + + alert = _waitForAlert(driver) + with pytest.raises(InvalidElementStateException): + alert.send_keys("cheese") + alert.accept() + + +@pytest.mark.xfail_chrome( + condition=sys.platform == 'darwin', + reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=26', + run=False) +def testAlertShouldNotAllowAdditionalCommandsIfDimissed(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "alert").click() + + alert = _waitForAlert(driver) + alert.dismiss() + + with pytest.raises(NoAlertPresentException): + alert.text + + +@pytest.mark.xfail_marionette(reason='Fails on travis') +@pytest.mark.xfail_remote(reason='Fails on travis') +def testShouldAllowUsersToAcceptAnAlertInAFrame(driver, pages): + pages.load("alerts.html") + driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert")) + driver.find_element_by_id("alertInFrame").click() + + alert = _waitForAlert(driver) + alert.accept() + + assert "Testing Alerts" == driver.title + + +@pytest.mark.xfail_marionette(reason='Fails on travis') +@pytest.mark.xfail_remote(reason='Fails on travis') +def testShouldAllowUsersToAcceptAnAlertInANestedFrame(driver, pages): + pages.load("alerts.html") + driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithIframe")) + driver.switch_to.frame(driver.find_element(By.NAME, "iframeWithAlert")) + + driver.find_element_by_id("alertInFrame").click() + + alert = _waitForAlert(driver) + alert.accept() + + assert "Testing Alerts" == driver.title + + +def testShouldThrowAnExceptionIfAnAlertHasNotBeenDealtWithAndDismissTheAlert(): + pass + # //TODO(David) Complete this test + + +def testPromptShouldUseDefaultValueIfNoKeysSent(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "prompt-with-default").click() + + alert = _waitForAlert(driver) + alert.accept() + + txt = driver.find_element(By.ID, "text").text + assert "This is a default value" == txt + + +def testPromptShouldHaveNullValueIfDismissed(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "prompt-with-default").click() + alert = _waitForAlert(driver) + alert.dismiss() + + assert "null" == driver.find_element(By.ID, "text").text + + +@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1477977') +@pytest.mark.xfail_remote(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1477977') +def testHandlesTwoAlertsFromOneInteraction(driver, pages): + pages.load("alerts.html") + + driver.find_element(By.ID, "double-prompt").click() + + alert1 = _waitForAlert(driver) + alert1.send_keys("brie") + alert1.accept() + + alert2 = _waitForAlert(driver) + alert2.send_keys("cheddar") + alert2.accept() + + assert driver.find_element(By.ID, "text1").text == "brie" + assert driver.find_element(By.ID, "text2").text == "cheddar" + + +def testShouldHandleAlertOnPageLoad(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "open-page-with-onload-alert").click() + alert = _waitForAlert(driver) + value = alert.text + alert.accept() + assert "onload" == value + + +def testShouldHandleAlertOnPageLoadUsingGet(driver, pages): + pages.load("pageWithOnLoad.html") + alert = _waitForAlert(driver) + value = alert.text + alert.accept() + + assert "onload" == value + WebDriverWait(driver, 3).until(EC.text_to_be_present_in_element((By.TAG_NAME, "p"), "Page with onload event handler")) + + +@pytest.mark.xfail_firefox(reason='Non W3C conformant') +@pytest.mark.xfail_chrome(reason='Non W3C conformant') +def testShouldHandleAlertOnPageBeforeUnload(driver, pages): + pages.load("pageWithOnBeforeUnloadMessage.html") + + element = driver.find_element(By.ID, "navigate") + element.click() + WebDriverWait(driver, 3).until(EC.title_is("Testing Alerts")) + + +def testShouldAllowTheUserToGetTheTextOfAnAlert(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="alert").click() + alert = _waitForAlert(driver) + value = alert.text + alert.accept() + assert "cheese" == value + + +def testShouldAllowTheUserToGetTheTextOfAPrompt(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "prompt").click() + + alert = _waitForAlert(driver) + value = alert.text + alert.accept() + + assert "Enter something" == value + + +def testAlertShouldNotAllowAdditionalCommandsIfDismissed(driver, pages): + pages.load("alerts.html") + driver.find_element(By.ID, "alert").click() + + alert = _waitForAlert(driver) + alert.accept() + + with pytest.raises(NoAlertPresentException): + alert.text + + +@pytest.mark.xfail_chrome( + reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=1537') +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1279211') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1279211') +def testUnexpectedAlertPresentExceptionContainsAlertText(driver, pages): + pages.load("alerts.html") + driver.find_element(by=By.ID, value="alert").click() + alert = _waitForAlert(driver) + value = alert.text + with pytest.raises(UnexpectedAlertPresentException) as e: + pages.load("simpleTest.html") + assert value == e.value.alert_text + assert "Alert Text: {}".format(value) in str(e) + + +def _waitForAlert(driver): + return WebDriverWait(driver, 3).until(EC.alert_is_present()) diff --git a/test/selenium/webdriver/common/api_example_tests.py b/test/selenium/webdriver/common/api_example_tests.py new file mode 100644 index 00000000..679c9be2 --- /dev/null +++ b/test/selenium/webdriver/common/api_example_tests.py @@ -0,0 +1,301 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Copyright 2008-2009 WebDriver committers +# Copyright 2008-2009 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from selenium.common.exceptions import ( + NoSuchElementException, + NoSuchWindowException, + TimeoutException, + WebDriverException) +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +def testGetTitle(driver, pages): + pages.load("simpleTest.html") + title = driver.title + assert "Hello WebDriver" == title + + +def testGetCurrentUrl(driver, pages, webserver): + pages.load("simpleTest.html") + url = driver.current_url + assert webserver.where_is('simpleTest.html') == url + + +def testFindElementsByXPath(driver, pages): + pages.load("simpleTest.html") + elem = driver.find_element_by_xpath("//h1") + assert "Heading" == elem.text + + +def testFindElementByXpathThrowNoSuchElementException(driver, pages): + pages.load("simpleTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element_by_xpath("//h4") + + +def testFindElementsByXpath(driver, pages): + pages.load("nestedElements.html") + elems = driver.find_elements_by_xpath("//option") + assert 48 == len(elems) + assert "One" == elems[0].get_attribute("value") + + +def testFindElementsByName(driver, pages): + pages.load("xhtmlTest.html") + elem = driver.find_element_by_name("windowOne") + assert "Open new window" == elem.text + + +def testFindElementsByNameInElementContext(driver, pages): + pages.load("nestedElements.html") + elem = driver.find_element_by_name("form2") + sub_elem = elem.find_element_by_name("selectomatic") + assert "2" == sub_elem.get_attribute("id") + + +def testFindElementsByLinkTextInElementContext(driver, pages): + pages.load("nestedElements.html") + elem = driver.find_element_by_name("div1") + sub_elem = elem.find_element_by_link_text("hello world") + assert "link1" == sub_elem.get_attribute("name") + + +def testFindElementByIdInElementContext(driver, pages): + pages.load("nestedElements.html") + elem = driver.find_element_by_name("form2") + sub_elem = elem.find_element_by_id("2") + assert "selectomatic" == sub_elem.get_attribute("name") + + +def testFindElementByXpathInElementContext(driver, pages): + pages.load("nestedElements.html") + elem = driver.find_element_by_name("form2") + sub_elem = elem.find_element_by_xpath("select") + assert "2" == sub_elem.get_attribute("id") + + +def testFindElementByXpathInElementContextNotFound(driver, pages): + pages.load("nestedElements.html") + elem = driver.find_element_by_name("form2") + with pytest.raises(NoSuchElementException): + elem.find_element_by_xpath("div") + + +def testShouldBeAbleToEnterDataIntoFormFields(driver, pages): + pages.load("xhtmlTest.html") + elem = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']") + elem.clear() + elem.send_keys("some text") + elem = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']") + assert "some text" == elem.get_attribute("value") + + +def testFindElementByTagName(driver, pages): + pages.load("simpleTest.html") + elems = driver.find_elements_by_tag_name("div") + num_by_xpath = len(driver.find_elements_by_xpath("//div")) + assert num_by_xpath == len(elems) + elems = driver.find_elements_by_tag_name("iframe") + assert 0 == len(elems) + + +def testFindElementByTagNameWithinElement(driver, pages): + pages.load("simpleTest.html") + div = driver.find_element_by_id("multiline") + elems = div.find_elements_by_tag_name("p") + assert len(elems) == 1 + + +@pytest.mark.xfail_marionette( + reason="W3C implementations can't switch to a window by name", + raises=TimeoutException, + run=False) +def testSwitchToWindow(driver, pages): + title_1 = "XHTML Test Page" + title_2 = "We Arrive Here" + switch_to_window_timeout = 5 + wait = WebDriverWait(driver, switch_to_window_timeout, ignored_exceptions=[NoSuchWindowException]) + pages.load("xhtmlTest.html") + driver.find_element_by_link_text("Open new window").click() + assert title_1 == driver.title + wait.until(lambda dr: dr.switch_to.window("result") is None) + assert title_2 == driver.title + + +def testSwitchFrameByName(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("third")) + checkbox = driver.find_element_by_id("checky") + checkbox.click() + checkbox.submit() + + +def testIsEnabled(driver, pages): + pages.load("formPage.html") + elem = driver.find_element_by_xpath("//input[@id='working']") + assert elem.is_enabled() + elem = driver.find_element_by_xpath("//input[@id='notWorking']") + assert not elem.is_enabled() + + +def testIsSelectedAndToggle(driver, pages): + pages.load("formPage.html") + elem = driver.find_element_by_id("multi") + option_elems = elem.find_elements_by_xpath("option") + assert option_elems[0].is_selected() + option_elems[0].click() + assert not option_elems[0].is_selected() + option_elems[0].click() + assert option_elems[0].is_selected() + assert option_elems[2].is_selected() + + +def testNavigate(driver, pages): + pages.load("formPage.html") + driver.find_element_by_id("imageButton").submit() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + driver.back() + assert "We Leave From Here" == driver.title + driver.forward() + assert "We Arrive Here" == driver.title + + +def testGetAttribute(driver, pages): + url = pages.url('xhtmlTest.html') + driver.get(url) + elem = driver.find_element_by_id("id1") + attr = elem.get_attribute("href") + assert '{0}#'.format(url) == attr + + +def testGetImplicitAttribute(driver, pages): + pages.load("nestedElements.html") + elems = driver.find_elements_by_xpath("//option") + assert len(elems) >= 3 + for i, elem in enumerate(elems[:3]): + assert i == int(elem.get_attribute("index")) + + +def testExecuteSimpleScript(driver, pages): + pages.load("xhtmlTest.html") + title = driver.execute_script("return document.title;") + assert "XHTML Test Page" == title + + +def testExecuteScriptAndReturnElement(driver, pages): + pages.load("xhtmlTest.html") + elem = driver.execute_script("return document.getElementById('id1');") + assert "WebElement" in str(type(elem)) + + +def testExecuteScriptWithArgs(driver, pages): + pages.load("xhtmlTest.html") + result = driver.execute_script("return arguments[0] == 'fish' ? 'fish' : 'not fish';", "fish") + assert "fish" == result + + +def testExecuteScriptWithMultipleArgs(driver, pages): + pages.load("xhtmlTest.html") + result = driver.execute_script( + "return arguments[0] + arguments[1]", 1, 2) + assert 3 == result + + +def testExecuteScriptWithElementArgs(driver, pages): + pages.load("javascriptPage.html") + button = driver.find_element_by_id("plainButton") + result = driver.execute_script("arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble'];", button) + assert "plainButton" == result + + +def testFindElementsByPartialLinkText(driver, pages): + pages.load("xhtmlTest.html") + elem = driver.find_element_by_partial_link_text("new window") + elem.click() + + +def testIsElementDisplayed(driver, pages): + pages.load("javascriptPage.html") + visible = driver.find_element_by_id("displayed").is_displayed() + not_visible = driver.find_element_by_id("hidden").is_displayed() + assert visible + assert not not_visible + + +def testMoveWindowPosition(driver, pages): + pages.load("blank.html") + loc = driver.get_window_position() + # note can't test 0,0 since some OS's dont allow that location + # because of system toolbars + new_x = 50 + new_y = 50 + if loc['x'] == new_x: + new_x += 10 + if loc['y'] == new_y: + new_y += 10 + driver.set_window_position(new_x, new_y) + loc = driver.get_window_position() + assert loc['x'] == new_x + assert loc['y'] == new_y + + +def testChangeWindowSize(driver, pages): + pages.load("blank.html") + size = driver.get_window_size() + newSize = [600, 600] + if size['width'] == 600: + newSize[0] = 500 + if size['height'] == 600: + newSize[1] = 500 + driver.set_window_size(newSize[0], newSize[1]) + size = driver.get_window_size() + assert size['width'] == newSize[0] + assert size['height'] == newSize[1] + + +@pytest.mark.xfail_chrome(raises=WebDriverException) +@pytest.mark.xfail_marionette(raises=WebDriverException) +def testGetLogTypes(driver, pages): + pages.load("blank.html") + assert isinstance(driver.log_types, list) + + +@pytest.mark.xfail_chrome(raises=WebDriverException) +@pytest.mark.xfail_marionette(raises=WebDriverException) +def testGetLog(driver, pages): + pages.load("blank.html") + for log_type in driver.log_types: + log = driver.get_log(log_type) + assert isinstance(log, list) diff --git a/test/selenium/webdriver/common/appcache_tests.py b/test/selenium/webdriver/common/appcache_tests.py new file mode 100644 index 00000000..946a33b6 --- /dev/null +++ b/test/selenium/webdriver/common/appcache_tests.py @@ -0,0 +1,36 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.html5.application_cache import ApplicationCache + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_marionette(raises=WebDriverException) +@pytest.mark.xfail_remote +def testWeCanGetTheStatusOfTheAppCache(driver, pages): + pages.load('html5Page') + driver.implicitly_wait(2) + app_cache = driver.application_cache + + status = app_cache.status + while status == ApplicationCache.DOWNLOADING: + status = app_cache.status + + assert ApplicationCache.UNCACHED == app_cache.status diff --git a/test/selenium/webdriver/common/children_finding_tests.py b/test/selenium/webdriver/common/children_finding_tests.py new file mode 100755 index 00000000..47a244bf --- /dev/null +++ b/test/selenium/webdriver/common/children_finding_tests.py @@ -0,0 +1,183 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import ( + WebDriverException, + NoSuchElementException) + + +def test_should_find_element_by_xpath(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + child = element.find_element_by_xpath("select") + assert child.get_attribute("id") == "2" + + +def test_should_not_find_element_by_xpath(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + with pytest.raises(NoSuchElementException): + element.find_element_by_xpath("select/x") + + +def test_finding_dot_slash_elements_on_element_by_xpath_should_find_not_top_level_elements(driver, pages): + pages.load("simpleTest.html") + parent = driver.find_element_by_id("multiline") + children = parent.find_elements_by_xpath("./p") + assert 1 == len(children) + assert "A div containing" == children[0].text + + +def test_should_find_elements_by_xpath(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + children = element.find_elements_by_xpath("select/option") + assert len(children) == 8 + assert children[0].text == "One" + assert children[1].text == "Two" + + +def test_should_not_find_elements_by_xpath(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + children = element.find_elements_by_xpath("select/x") + assert len(children) == 0 + + +def test_finding_elements_on_element_by_xpath_should_find_top_level_elements(driver, pages): + pages.load("simpleTest.html") + parent = driver.find_element_by_id("multiline") + all_para_elements = driver.find_elements_by_xpath("//p") + children = parent.find_elements_by_xpath("//p") + assert len(all_para_elements) == len(children) + + +def test_should_find_element_by_name(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + child = element.find_element_by_name("selectomatic") + assert child.get_attribute("id") == "2" + + +def test_should_find_elements_by_name(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + children = element.find_elements_by_name("selectomatic") + assert len(children) == 2 + + +def test_should_find_element_by_id(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + child = element.find_element_by_id("2") + assert child.get_attribute("name") == "selectomatic" + + +def test_should_find_elements_by_id(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + child = element.find_elements_by_id("2") + assert len(child) == 2 + + +def test_should_find_element_by_id_when_multiple_matches_exist(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_id("test_id_div") + child = element.find_element_by_id("test_id") + assert child.text == "inside" + + +def test_should_find_element_by_id_when_no_match_in_context(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_id("test_id_div") + with pytest.raises(NoSuchElementException): + element.find_element_by_id("test_id_out") + + +def test_should_find_element_by_link_text(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("div1") + child = element.find_element_by_link_text("hello world") + assert child.get_attribute("name") == "link1" + + +def test_should_find_elements_by_link_text(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("div1") + children = element.find_elements_by_link_text("hello world") + assert len(children) == 2 + assert "link1" == children[0].get_attribute("name") + assert "link2" == children[1].get_attribute("name") + + +def test_should_find_element_by_class_name(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("classes") + element = parent.find_element_by_class_name("one") + assert "Find me" == element.text + + +def test_should_find_elements_by_class_name(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("classes") + elements = parent.find_elements_by_class_name("one") + assert 2 == len(elements) + + +def test_should_find_element_by_tag_name(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("div1") + element = parent.find_element_by_tag_name("a") + assert "link1" == element.get_attribute("name") + + +def test_should_find_elements_by_tag_name(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("div1") + elements = parent.find_elements_by_tag_name("a") + assert 2 == len(elements) + + +def test_should_be_able_to_find_an_element_by_css_selector(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("form2") + element = parent.find_element_by_css_selector('*[name="selectomatic"]') + assert "2" == element.get_attribute("id") + + +def test_should_be_able_to_find_multiple_elements_by_css_selector(driver, pages): + pages.load("nestedElements.html") + parent = driver.find_element_by_name("form2") + elements = parent.find_elements_by_css_selector( + '*[name="selectomatic"]') + assert 2 == len(elements) + + +def test_should_throw_an_error_if_user_passes_in_invalid_by(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + with pytest.raises(WebDriverException): + element.find_element("foo", "bar") + + +def test_should_throw_an_error_if_user_passes_in_invalid_by_when_find_elements(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element_by_name("form2") + with pytest.raises(WebDriverException): + element.find_elements("foo", "bar") diff --git a/test/selenium/webdriver/common/clear_tests.py b/test/selenium/webdriver/common/clear_tests.py new file mode 100644 index 00000000..b650f17b --- /dev/null +++ b/test/selenium/webdriver/common/clear_tests.py @@ -0,0 +1,70 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import InvalidElementStateException + + +def testWritableTextInputShouldClear(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("writableTextInput") + element.clear() + assert "" == element.get_attribute("value") + + +def testTextInputShouldNotClearWhenDisabled(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("textInputnotenabled") + assert not element.is_enabled() + with pytest.raises(InvalidElementStateException): + element.clear() + + +def testTextInputShouldNotClearWhenReadOnly(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("readOnlyTextInput") + with pytest.raises(InvalidElementStateException): + element.clear() + + +def testWritableTextAreaShouldClear(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("writableTextArea") + element.clear() + assert "" == element.get_attribute("value") + + +def testTextAreaShouldNotClearWhenDisabled(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("textAreaNotenabled") + with pytest.raises(InvalidElementStateException): + element.clear() + + +def testTextAreaShouldNotClearWhenReadOnly(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("textAreaReadOnly") + with pytest.raises(InvalidElementStateException): + element.clear() + + +def testContentEditableAreaShouldClear(driver, pages): + pages.load("readOnlyPage.html") + element = driver.find_element_by_id("content-editable") + element.clear() + assert "" == element.text diff --git a/test/selenium/webdriver/common/click_scrolling_tests.py b/test/selenium/webdriver/common/click_scrolling_tests.py new file mode 100644 index 00000000..bc8adb8f --- /dev/null +++ b/test/selenium/webdriver/common/click_scrolling_tests.py @@ -0,0 +1,186 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import MoveTargetOutOfBoundsException +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +def testClickingOnAnchorScrollsPage(driver, pages): + scrollScript = """var pageY; + if (typeof(window.pageYOffset) == 'number') { + pageY = window.pageYOffset; + } else { + pageY = document.documentElement.scrollTop; + } + return pageY;""" + + pages.load("macbeth.html") + + driver.find_element(By.PARTIAL_LINK_TEXT, "last speech").click() + + yOffset = driver.execute_script(scrollScript) + + # Focusing on to click, but not actually following, + # the link will scroll it in to view, which is a few pixels further than 0 + assert yOffset > 300 + + +def testShouldScrollToClickOnAnElementHiddenByOverflow(driver, pages): + pages.load("click_out_of_bounds_overflow.html") + + link = driver.find_element(By.ID, "link") + try: + link.click() + except MoveTargetOutOfBoundsException as e: + AssertionError("Should not be out of bounds: %s" % e.msg) + + +def testShouldBeAbleToClickOnAnElementHiddenByOverflow(driver, pages): + pages.load("scroll.html") + + link = driver.find_element(By.ID, "line8") + # This used to throw a MoveTargetOutOfBoundsException - we don't expect it to + link.click() + assert "line8" == driver.find_element(By.ID, "clicked").text + + +def testShouldBeAbleToClickOnAnElementHiddenByDoubleOverflow(driver, pages): + pages.load("scrolling_tests/page_with_double_overflow_auto.html") + + driver.find_element(By.ID, "link").click() + WebDriverWait(driver, 3).until(EC.title_is("Clicked Successfully!")) + + +def testShouldBeAbleToClickOnAnElementHiddenByYOverflow(driver, pages): + pages.load("scrolling_tests/page_with_y_overflow_auto.html") + + driver.find_element(By.ID, "link").click() + WebDriverWait(driver, 3).until(EC.title_is("Clicked Successfully!")) + + +def testShouldNotScrollOverflowElementsWhichAreVisible(driver, pages): + pages.load("scroll2.html") + list = driver.find_element(By.TAG_NAME, "ul") + item = list.find_element(By.ID, "desired") + item.click() + yOffset = driver.execute_script("return arguments[0].scrollTop", list) + assert 0 == yOffset, "Should not have scrolled" + + +@pytest.mark.xfail_chrome( + reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=1542') +@pytest.mark.xfail_marionette +@pytest.mark.xfail_remote +def testShouldNotScrollIfAlreadyScrolledAndElementIsInView(driver, pages): + pages.load("scroll3.html") + driver.find_element(By.ID, "button1").click() + scrollTop = getScrollTop(driver) + driver.find_element(By.ID, "button2").click() + assert scrollTop == getScrollTop(driver) + + +def testShouldBeAbleToClickRadioButtonScrolledIntoView(driver, pages): + pages.load("scroll4.html") + driver.find_element(By.ID, "radio").click() + # If we don't throw, we're good + + +def testShouldScrollOverflowElementsIfClickPointIsOutOfViewButElementIsInView(driver, pages): + pages.load("scroll5.html") + driver.find_element(By.ID, "inner").click() + assert "clicked" == driver.find_element(By.ID, "clicked").text + + +@pytest.mark.xfail_marionette( + reason='https://github.com/w3c/webdriver/issues/408') +@pytest.mark.xfail_remote( + reason='https://github.com/w3c/webdriver/issues/408') +def testShouldBeAbleToClickElementInAFrameThatIsOutOfView(driver, pages): + pages.load("scrolling_tests/page_with_frame_out_of_view.html") + driver.switch_to.frame(driver.find_element_by_name("frame")) + element = driver.find_element(By.NAME, "checkbox") + element.click() + assert element.is_selected() + + +def testShouldBeAbleToClickElementThatIsOutOfViewInAFrame(driver, pages): + pages.load("scrolling_tests/page_with_scrolling_frame.html") + driver.switch_to.frame(driver.find_element_by_name("scrolling_frame")) + element = driver.find_element(By.NAME, "scroll_checkbox") + element.click() + assert element.is_selected() + + +def testShouldNotBeAbleToClickElementThatIsOutOfViewInANonScrollableFrame(driver, pages): + pages.load("scrolling_tests/page_with_non_scrolling_frame.html") + driver.switch_to.frame("scrolling_frame") + element = driver.find_element(By.NAME, "scroll_checkbox") + element.click() + # TODO we should assert that the click was unsuccessful + + +def testShouldBeAbleToClickElementThatIsOutOfViewInAFrameThatIsOutOfView(driver, pages): + pages.load("scrolling_tests/page_with_scrolling_frame_out_of_view.html") + driver.switch_to.frame(driver.find_element_by_name("scrolling_frame")) + element = driver.find_element(By.NAME, "scroll_checkbox") + element.click() + assert element.is_selected() + + +def testShouldBeAbleToClickElementThatIsOutOfViewInANestedFrame(driver, pages): + pages.load("scrolling_tests/page_with_nested_scrolling_frames.html") + driver.switch_to.frame(driver.find_element_by_name("scrolling_frame")) + driver.switch_to.frame(driver.find_element_by_name("nested_scrolling_frame")) + element = driver.find_element(By.NAME, "scroll_checkbox") + element.click() + assert element.is_selected() + + +def testShouldBeAbleToClickElementThatIsOutOfViewInANestedFrameThatIsOutOfView(driver, pages): + pages.load("scrolling_tests/page_with_nested_scrolling_frames_out_of_view.html") + driver.switch_to.frame(driver.find_element_by_name("scrolling_frame")) + driver.switch_to.frame(driver.find_element_by_name("nested_scrolling_frame")) + element = driver.find_element(By.NAME, "scroll_checkbox") + element.click() + assert element.is_selected() + + +def testShouldNotScrollWhenGettingElementSize(driver, pages): + pages.load("scroll3.html") + scrollTop = getScrollTop(driver) + driver.find_element(By.ID, "button1").size + assert scrollTop == getScrollTop(driver) + + +def getScrollTop(driver): + return driver.execute_script("return document.body.scrollTop") + + +@pytest.mark.xfail_marionette( + reason='https://github.com/w3c/webdriver/issues/408') +@pytest.mark.xfail_remote( + reason='https://github.com/w3c/webdriver/issues/408') +def testShouldBeAbleToClickElementInATallFrame(driver, pages): + pages.load("scrolling_tests/page_with_tall_frame.html") + driver.switch_to.frame(driver.find_element_by_name("tall_frame")) + element = driver.find_element(By.NAME, "checkbox") + element.click() + assert element.is_selected() diff --git a/test/selenium/webdriver/common/click_tests.py b/test/selenium/webdriver/common/click_tests.py new file mode 100644 index 00000000..5f4458fd --- /dev/null +++ b/test/selenium/webdriver/common/click_tests.py @@ -0,0 +1,37 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait + + +@pytest.fixture(autouse=True) +def loadPage(pages): + pages.load("clicks.html") + + +def testCanClickOnALinkThatOverflowsAndFollowIt(driver): + driver.find_element(By.ID, "overflowLink").click() + WebDriverWait(driver, 3).until(EC.title_is("XHTML Test Page")) + + +def testClickingALinkMadeUpOfNumbersIsHandledCorrectly(driver): + driver.find_element(By.LINK_TEXT, "333333").click() + WebDriverWait(driver, 3).until(EC.title_is("XHTML Test Page")) diff --git a/test/selenium/webdriver/common/conftest.py b/test/selenium/webdriver/common/conftest.py new file mode 100644 index 00000000..75670927 --- /dev/null +++ b/test/selenium/webdriver/common/conftest.py @@ -0,0 +1,21 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def pytest_generate_tests(metafunc): + if 'driver' in metafunc.fixturenames and metafunc.config.option.drivers: + metafunc.parametrize('driver', metafunc.config.option.drivers, indirect=True) diff --git a/test/selenium/webdriver/common/cookie_tests.py b/test/selenium/webdriver/common/cookie_tests.py new file mode 100644 index 00000000..a1c0970f --- /dev/null +++ b/test/selenium/webdriver/common/cookie_tests.py @@ -0,0 +1,102 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import calendar +import time +import random + +import pytest + + +@pytest.fixture +def cookie(webserver): + cookie = { + 'name': 'foo', + 'value': 'bar', + 'domain': webserver.host, + 'path': '/', + 'secure': False} + return cookie + + +@pytest.fixture(autouse=True) +def pages(request, driver, pages): + pages.load('simpleTest.html') + yield pages + driver.delete_all_cookies() + + +def testAddCookie(cookie, driver): + driver.add_cookie(cookie) + returned = driver.execute_script('return document.cookie') + assert cookie['name'] in returned + + +@pytest.mark.xfail_ie +def testAddingACookieThatExpiredInThePast(cookie, driver): + expired = cookie.copy() + expired['expiry'] = calendar.timegm(time.gmtime()) - 1 + driver.add_cookie(expired) + assert 0 == len(driver.get_cookies()) + + +def testDeleteAllCookie(cookie, driver): + driver.add_cookie(cookie) + driver.delete_all_cookies() + assert not driver.get_cookies() + + +def testDeleteCookie(cookie, driver): + driver.add_cookie(cookie) + driver.delete_cookie('foo') + assert not driver.get_cookies() + + +def testShouldGetCookieByName(driver): + key = 'key_{}'.format(int(random.random() * 10000000)) + driver.execute_script("document.cookie = arguments[0] + '=set';", key) + cookie = driver.get_cookie(key) + assert 'set' == cookie['value'] + + +def testShouldReturnNoneWhenCookieDoesNotExist(driver): + key = 'key_{}'.format(int(random.random() * 10000000)) + cookie = driver.get_cookie(key) + assert cookie is None + + +def testGetAllCookies(cookie, driver, pages, webserver): + cookies = driver.get_cookies() + count = len(cookies) + + for i in range(2): + cookie['name'] = 'key_{}'.format(int(random.random() * 10000000)) + driver.add_cookie(cookie) + + pages.load('simpleTest.html') + assert count + 2 == len(driver.get_cookies()) + + +def testShouldNotDeleteCookiesWithASimilarName(cookie, driver, webserver): + cookie2 = cookie.copy() + cookie2['name'] = '{}x'.format(cookie['name']) + driver.add_cookie(cookie) + driver.add_cookie(cookie2) + driver.delete_cookie(cookie['name']) + cookies = driver.get_cookies() + assert cookie['name'] != cookies[0]['name'] + assert cookie2['name'] == cookies[0]['name'] diff --git a/test/selenium/webdriver/common/correct_event_firing_tests.py b/test/selenium/webdriver/common/correct_event_firing_tests.py new file mode 100644 index 00000000..1a6af378 --- /dev/null +++ b/test/selenium/webdriver/common/correct_event_firing_tests.py @@ -0,0 +1,123 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def testShouldFireClickEventWhenClicking(driver, pages): + pages.load("javascriptPage.html") + _clickOnElementWhichRecordsEvents(driver) + _assertEventFired(driver, "click") + + +def testShouldFireMouseDownEventWhenClicking(driver, pages): + pages.load("javascriptPage.html") + _clickOnElementWhichRecordsEvents(driver) + _assertEventFired(driver, "mousedown") + + +def testShouldFireMouseUpEventWhenClicking(driver, pages): + pages.load("javascriptPage.html") + _clickOnElementWhichRecordsEvents(driver) + _assertEventFired(driver, "mouseup") + + +def testShouldIssueMouseDownEvents(driver, pages): + pages.load("javascriptPage.html") + driver.find_element_by_id("mousedown").click() + result = driver.find_element_by_id("result").text + assert result == "mouse down" + + +def testShouldIssueClickEvents(driver, pages): + pages.load("javascriptPage.html") + driver.find_element_by_id("mouseclick").click() + result = driver.find_element_by_id("result").text + assert result == "mouse click" + + +def testShouldIssueMouseUpEvents(driver, pages): + pages.load("javascriptPage.html") + driver.find_element_by_id("mouseup").click() + result = driver.find_element_by_id("result").text + assert result == "mouse up" + + +def testMouseEventsShouldBubbleUpToContainingElements(driver, pages): + pages.load("javascriptPage.html") + driver.find_element_by_id("child").click() + result = driver.find_element_by_id("result").text + assert result == "mouse down" + + +def testShouldEmitOnChangeEventsWhenSelectingElements(driver, pages): + pages.load("javascriptPage.html") + select = driver.find_element_by_id('selector') + options = select.find_elements_by_tag_name('option') + initialTextValue = driver.find_element_by_id("result").text + + select.click() + assert driver.find_element_by_id("result").text == initialTextValue + options[1].click() + assert driver.find_element_by_id("result").text == "bar" + + +def testShouldEmitOnChangeEventsWhenChangingTheStateOfACheckbox(driver, pages): + pages.load("javascriptPage.html") + checkbox = driver.find_element_by_id("checkbox") + checkbox.click() + assert driver.find_element_by_id("result").text == "checkbox thing" + + +def testShouldEmitClickEventWhenClickingOnATextInputElement(driver, pages): + pages.load("javascriptPage.html") + clicker = driver.find_element_by_id("clickField") + clicker.click() + + assert clicker.get_attribute("value") == "Clicked" + + +def testClearingAnElementShouldCauseTheOnChangeHandlerToFire(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element_by_id("clearMe") + element.clear() + result = driver.find_element_by_id("result") + assert result.text == "Cleared" + +# TODO Currently Failing and needs fixing +# def testSendingKeysToAnotherElementShouldCauseTheBlurEventToFire(driver, pages): +# pages.load("javascriptPage.html") +# element = driver.find_element_by_id("theworks") +# element.send_keys("foo") +# element2 = driver.find_element_by_id("changeable") +# element2.send_keys("bar") +# _assertEventFired(driver, "blur") + +# TODO Currently Failing and needs fixing +# def testSendingKeysToAnElementShouldCauseTheFocusEventToFire(driver, pages): +# pages.load("javascriptPage.html") +# element = driver.find_element_by_id("theworks") +# element.send_keys("foo") +# _assertEventFired(driver, "focus") + + +def _clickOnElementWhichRecordsEvents(driver): + driver.find_element_by_id("plainButton").click() + + +def _assertEventFired(driver, eventName): + result = driver.find_element_by_id("result") + text = result.text + assert eventName in text, "No " + eventName + " fired: " + text diff --git a/test/selenium/webdriver/common/driver_element_finding_tests.py b/test/selenium/webdriver/common/driver_element_finding_tests.py new file mode 100755 index 00000000..a1402f9e --- /dev/null +++ b/test/selenium/webdriver/common/driver_element_finding_tests.py @@ -0,0 +1,629 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By +from selenium.common.exceptions import ( + InvalidSelectorException, + NoSuchElementException, + WebDriverException) + +# By.id positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Id(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.ID, "linkId") + assert element.get_attribute("id") == "linkId" + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Numeric_Id(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element(By.ID, "2") + assert element.get_attribute("id") == "2" + + +def test_should_be_able_to_find_an_element_with_css_escape(driver, pages): + pages.load("idElements.html") + element = driver.find_element(By.ID, "with.dots") + assert element.get_attribute("id") == "with.dots" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Id(driver, pages): + pages.load("nestedElements.html") + elements = driver.find_elements(By.ID, "test_id") + assert len(elements) == 2 + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Numeric_Id(driver, pages): + pages.load("nestedElements.html") + elements = driver.find_elements(By.ID, "2") + assert len(elements) == 8 + +# By.id negative + + +def test_Should_Not_Be_Able_To_Locate_By_Id_ASingle_Element_That_Does_Not_Exist(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.ID, "non_Existent_Button") + + +def test_Should_Not_Be_Able_To_Locate_By_Id_Multiple_Elements_That_Do_Not_Exist(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.ID, "non_Existent_Button") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Empty_Id_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.ID, "") + + +def test_Finding_Multiple_Elements_By_Empty_Id_Should_Return_Empty_List(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.ID, "") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Id_With_Space_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.ID, "nonexistent button") + + +def test_Finding_Multiple_Elements_By_Id_With_Space_Should_Return_Empty_List(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.ID, "nonexistent button") + assert len(elements) == 0 + +# By.name positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Name(driver, pages): + pages.load("formPage.html") + element = driver.find_element(By.NAME, "checky") + assert element.get_attribute("value") == "furrfu" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Name(driver, pages): + pages.load("nestedElements.html") + elements = driver.find_elements(By.NAME, "checky") + assert len(elements) > 1 + + +def test_Should_Be_Able_To_Find_An_Element_That_Does_Not_Support_The_Name_Property(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element(By.NAME, "div1") + assert element.get_attribute("name") == "div1" + +# By.name negative + + +def test_Should_Not_Be_Able_To_Locate_By_Name_ASingle_Element_That_Does_Not_Exist(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.NAME, "non_Existent_Button") + + +def test_Should_Not_Be_Able_To_Locate_By_Name_Multiple_Elements_That_Do_Not_Exist(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.NAME, "non_Existent_Button") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Empty_Name_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.NAME, "") + + +def test_Finding_Multiple_Elements_By_Empty_Name_Should_Return_Empty_List(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.NAME, "") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Name_With_Space_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.NAME, "nonexistent button") + + +def test_Finding_Multiple_Elements_By_Name_With_Space_Should_Return_Empty_List(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.NAME, "nonexistent button") + assert len(elements) == 0 + +# By.tag_Name positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Tag_Name(driver, pages): + pages.load("formPage.html") + element = driver.find_element(By.TAG_NAME, "input") + assert element.tag_name.lower() == "input" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Tag_Name(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.TAG_NAME, "input") + assert len(elements) > 1 + +# By.tag_Name negative + + +def test_Should_Not_Be_Able_To_Locate_By_Tag_Name_ASingle_Element_That_Does_Not_Exist(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.TAG_NAME, "non_Existent_Button") + + +def test_Should_Not_Be_Able_To_Locate_By_Tag_Name_Multiple_Elements_That_Do_Not_Exist(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.TAG_NAME, "non_Existent_Button") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Empty_Tag_Name_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_element(By.TAG_NAME, "") + + +def test_Finding_Multiple_Elements_By_Empty_Tag_Name_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_elements(By.TAG_NAME, "") + + +def test_Finding_ASingle_Element_By_Tag_Name_With_Space_Should_Throw(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.TAG_NAME, "nonexistent button") + + +def test_Finding_Multiple_Elements_By_Tag_Name_With_Space_Should_Return_Empty_List(driver, pages): + pages.load("formPage.html") + elements = driver.find_elements(By.TAG_NAME, "nonexistent button") + assert len(elements) == 0 + +# By.class_Name positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Class(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CLASS_NAME, "extraDiv") + assert "Another div starts here." in element.text + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Class_Name(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.CLASS_NAME, "nameC") + assert len(elements) > 1 + + +def test_Should_Find_Element_By_Class_When_It_Is_The_First_Name_Among_Many(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CLASS_NAME, "nameA") + assert element.text == "An H2 title" + + +def test_Should_Find_Element_By_Class_When_It_Is_The_Last_Name_Among_Many(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CLASS_NAME, "nameC") + assert element.text == "An H2 title" + + +def test_Should_Find_Element_By_Class_When_It_Is_In_The_Middle_Among_Many(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CLASS_NAME, "nameBnoise") + assert element.text == "An H2 title" + + +def test_Should_Find_Element_By_Class_When_Its_Name_Is_Surrounded_By_Whitespace(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CLASS_NAME, "spaceAround") + assert element.text == "Spaced out" + + +def test_Should_Find_Elements_By_Class_When_Its_Name_Is_Surrounded_By_Whitespace(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.CLASS_NAME, "spaceAround") + assert len(elements) == 1 + assert elements[0].text == "Spaced out" + +# By.class_Name negative + + +def test_Should_Not_Find_Element_By_Class_When_The_Name_Queried_Is_Shorter_Than_Candidate_Name(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CLASS_NAME, "name_B") + + +def test_Finding_ASingle_Element_By_Empty_Class_Name_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CLASS_NAME, "") + + +def test_Finding_Multiple_Elements_By_Empty_Class_Name_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_elements(By.CLASS_NAME, "") + + +def test_Finding_ASingle_Element_By_Compound_Class_Name_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CLASS_NAME, "a b") + + +def test_Finding_ASingle_Element_By_Invalid_Class_Name_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CLASS_NAME, "!@#$%^&*") + + +def test_Finding_Multiple_Elements_By_Invalid_Class_Name_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_elements(By.CLASS_NAME, "!@#$%^&*") + +# By.xpath positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_XPath(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.XPATH, "//h1") + assert element.text == "XHTML Might Be The Future" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_XPath(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.XPATH, "//div") + assert len(elements) == 13 + + +def test_Should_Be_Able_To_Find_Many_Elements_Repeatedly_By_XPath(driver, pages): + pages.load("xhtmlTest.html") + xpath = "//node()[contains(@id,'id')]" + assert len(driver.find_elements(By.XPATH, xpath)) == 3 + + xpath = "//node()[contains(@id,'nope')]" + assert len(driver.find_elements(By.XPATH, xpath)) == 0 + + +def test_Should_Be_Able_To_Identify_Elements_By_Class(driver, pages): + pages.load("xhtmlTest.html") + header = driver.find_element(By.XPATH, "//h1[@class='header']") + assert header.text == "XHTML Might Be The Future" + + +def test_Should_Be_Able_To_Find_An_Element_By_XPath_With_Multiple_Attributes(driver, pages): + pages.load("formPage.html") + element = driver.find_element( + By.XPATH, "//form[@name='optional']/input[@type='submit' and @value='Click!']") + assert element.tag_name.lower() == "input" + assert element.get_attribute("value") == "Click!" + + +def test_Finding_ALink_By_Xpath_Should_Locate_An_Element_With_The_Given_Text(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.XPATH, "//a[text()='click me']") + assert element.text == "click me" + + +def test_Finding_ALink_By_Xpath_Using_Contains_Keyword_Should_Work(driver, pages): + pages.load("nestedElements.html") + element = driver.find_element(By.XPATH, "//a[contains(.,'hello world')]") + assert "hello world" in element.text + + +@pytest.mark.xfail_chrome(raises=InvalidSelectorException) +@pytest.mark.xfail_firefox(raises=InvalidSelectorException) +@pytest.mark.xfail_remote(raises=InvalidSelectorException) +@pytest.mark.xfail_marionette(raises=WebDriverException) +@pytest.mark.xfail_safari(raises=NoSuchElementException) +@pytest.mark.xfail_webkitgtk(raises=InvalidSelectorException) +def test_Should_Be_Able_To_Find_Element_By_XPath_With_Namespace(driver, pages): + pages.load("svgPage.html") + element = driver.find_element(By.XPATH, "//svg:svg//svg:text") + assert element.text == "Test Chart" + + +def test_Should_Be_Able_To_Find_Element_By_XPath_In_Xml_Document(driver, pages): + pages.load("simple.xml") + element = driver.find_element(By.XPATH, "//foo") + assert "baz" in element.text + +# By.xpath negative + + +def test_Should_Throw_An_Exception_When_There_Is_No_Link_To_Click(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.XPATH, "//a[@id='Not here']") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Driver_Find_Element(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_element(By.XPATH, "this][isnot][valid") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Driver_Find_Elements(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_elements(By.XPATH, "this][isnot][valid") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Element_Find_Element(driver, pages): + pages.load("formPage.html") + body = driver.find_element(By.TAG_NAME, "body") + with pytest.raises(InvalidSelectorException): + body.find_element(By.XPATH, "this][isnot][valid") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Is_Syntactically_Invalid_In_Element_Find_Elements(driver, pages): + pages.load("formPage.html") + body = driver.find_element(By.TAG_NAME, "body") + with pytest.raises(InvalidSelectorException): + body.find_elements(By.XPATH, "this][isnot][valid") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Driver_Find_Element(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_element(By.XPATH, "count(//input)") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Driver_Find_Elements(driver, pages): + pages.load("formPage.html") + with pytest.raises(InvalidSelectorException): + driver.find_elements(By.XPATH, "count(//input)") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Element_Find_Element(driver, pages): + pages.load("formPage.html") + body = driver.find_element(By.TAG_NAME, "body") + with pytest.raises(InvalidSelectorException): + body.find_element(By.XPATH, "count(//input)") + + +def test_Should_Throw_InvalidSelectorException_When_XPath_Returns_Wrong_Type_In_Element_Find_Elements(driver, pages): + pages.load("formPage.html") + body = driver.find_element(By.TAG_NAME, "body") + with pytest.raises(InvalidSelectorException): + body.find_elements(By.XPATH, "count(//input)") + +# By.css_Selector positive + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Css_Selector(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CSS_SELECTOR, "div.content") + assert element.tag_name.lower() == "div" + assert element.get_attribute("class") == "content" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Css_Selector(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.CSS_SELECTOR, "p") + assert len(elements) > 1 + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Compound_Css_Selector(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.CSS_SELECTOR, "div.extraDiv, div.content") + assert element.tag_name.lower() == "div" + assert element.get_attribute("class") == "content" + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Compound_Css_Selector(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.CSS_SELECTOR, "div.extraDiv, div.content") + assert len(elements) > 1 + assert elements[0].get_attribute("class") == "content" + assert elements[1].get_attribute("class") == "extraDiv" + + +def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Css_Selector(driver, pages): + pages.load("locators_tests/boolean_attribute_selected.html") + element = driver.find_element(By.CSS_SELECTOR, "option[selected='selected']") + assert element.get_attribute("value") == "two" + + +def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Short_Css_Selector(driver, pages): + pages.load("locators_tests/boolean_attribute_selected.html") + element = driver.find_element(By.CSS_SELECTOR, "option[selected]") + assert element.get_attribute("value") == "two" + + +def test_Should_Be_Able_To_Find_An_Element_By_Boolean_Attribute_Using_Short_Css_Selector_On_Html4Page(driver, pages): + pages.load("locators_tests/boolean_attribute_selected_html4.html") + element = driver.find_element(By.CSS_SELECTOR, "option[selected]") + assert element.get_attribute("value") == "two" + +# By.css_Selector negative + + +def test_Should_Not_Find_Element_By_Css_Selector_When_There_Is_No_Such_Element(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CSS_SELECTOR, ".there-is-no-such-class") + + +def test_Should_Not_Find_Elements_By_Css_Selector_When_There_Is_No_Such_Element(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.CSS_SELECTOR, ".there-is-no-such-class") + assert len(elements) == 0 + + +def test_Finding_ASingle_Element_By_Empty_Css_Selector_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CSS_SELECTOR, "") + + +def test_Finding_Multiple_Elements_By_Empty_Css_Selector_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_elements(By.CSS_SELECTOR, "") + + +def test_Finding_ASingle_Element_By_Invalid_Css_Selector_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.CSS_SELECTOR, "//a/b/c[@id='1']") + + +def test_Finding_Multiple_Elements_By_Invalid_Css_Selector_Should_Throw(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_elements(By.CSS_SELECTOR, "//a/b/c[@id='1']") + +# By.link_Text positive + + +def test_Should_Be_Able_To_Find_ALink_By_Text(driver, pages): + pages.load("xhtmlTest.html") + link = driver.find_element(By.LINK_TEXT, "click me") + assert link.text == "click me" + + +def test_Should_Be_Able_To_Find_Multiple_Links_By_Text(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.LINK_TEXT, "click me") + assert len(elements) == 2 + + +def test_Should_Find_Element_By_Link_Text_Containing_Equals_Sign(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.LINK_TEXT, "Link=equalssign") + assert element.get_attribute("id") == "linkWithEqualsSign" + + +def test_Should_Find_Multiple_Elements_By_Link_Text_Containing_Equals_Sign(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.LINK_TEXT, "Link=equalssign") + assert 1 == len(elements) + assert elements[0].get_attribute("id") == "linkWithEqualsSign" + + +def test_finds_By_Link_Text_On_Xhtml_Page(driver, pages): + pages.load("actualXhtmlPage.xhtml") + link_Text = "Foo" + element = driver.find_element(By.LINK_TEXT, link_Text) + assert element.text == link_Text + + +def test_Link_With_Formatting_Tags(driver, pages): + pages.load("simpleTest.html") + elem = driver.find_element(By.ID, "links") + + res = elem.find_element(By.PARTIAL_LINK_TEXT, "link with formatting tags") + assert res.text == "link with formatting tags" + + +def test_Driver_Can_Get_Link_By_Link_Test_Ignoring_Trailing_Whitespace(driver, pages): + pages.load("simpleTest.html") + link = driver.find_element(By.LINK_TEXT, "link with trailing space") + assert link.get_attribute("id") == "linkWithTrailingSpace" + assert link.text == "link with trailing space" + +# By.link_Text negative + + +def test_Should_Not_Be_Able_To_Locate_By_Link_Text_ASingle_Element_That_Does_Not_Exist(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.find_element(By.LINK_TEXT, "Not here either") + + +def test_Should_Not_Be_Able_To_Locate_By_Link_Text_Multiple_Elements_That_Do_Not_Exist(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.LINK_TEXT, "Not here either") + assert len(elements) == 0 + +# By.partial_Link_Text positive + + +def test_Should_Be_Able_To_Find_Multiple_Elements_By_Partial_Link_Text(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.PARTIAL_LINK_TEXT, "ick me") + assert len(elements) == 2 + + +def test_Should_Be_Able_To_Find_ASingle_Element_By_Partial_Link_Text(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.PARTIAL_LINK_TEXT, "anon") + assert "anon" in element.text + + +def test_Should_Find_Element_By_Partial_Link_Text_Containing_Equals_Sign(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element(By.PARTIAL_LINK_TEXT, "Link=") + assert element.get_attribute("id") == "linkWithEqualsSign" + + +def test_Should_Find_Multiple_Elements_By_Partial_Link_Text_Containing_Equals_Sign(driver, pages): + pages.load("xhtmlTest.html") + elements = driver.find_elements(By.PARTIAL_LINK_TEXT, "Link=") + assert len(elements) == 1 + assert elements[0].get_attribute("id") == "linkWithEqualsSign" + +# Misc tests + + +def test_Driver_Should_Be_Able_To_Find_Elements_After_Loading_More_Than_One_Page_At_ATime(driver, pages): + pages.load("formPage.html") + pages.load("xhtmlTest.html") + link = driver.find_element(By.LINK_TEXT, "click me") + assert link.text == "click me" + +# You don't want to ask why this is here + + +def test_When_Finding_By_Name_Should_Not_Return_By_Id(driver, pages): + pages.load("formPage.html") + + element = driver.find_element(By.NAME, "id-name1") + assert element.get_attribute("value") == "name" + + element = driver.find_element(By.ID, "id-name1") + assert element.get_attribute("value") == "id" + + element = driver.find_element(By.NAME, "id-name2") + assert element.get_attribute("value") == "name" + + element = driver.find_element(By.ID, "id-name2") + assert element.get_attribute("value") == "id" + + +def test_Should_Be_Able_To_Find_AHidden_Elements_By_Name(driver, pages): + pages.load("formPage.html") + element = driver.find_element(By.NAME, "hidden") + assert element.get_attribute("name") == "hidden" + + +def test_Should_Not_Be_Able_To_Find_An_Element_On_ABlank_Page(driver, pages): + driver.get("about:blank") + with pytest.raises(NoSuchElementException): + driver.find_element(By.TAG_NAME, "a") diff --git a/test/selenium/webdriver/common/element_attribute_tests.py b/test/selenium/webdriver/common/element_attribute_tests.py new file mode 100644 index 00000000..bc6dd8ab --- /dev/null +++ b/test/selenium/webdriver/common/element_attribute_tests.py @@ -0,0 +1,272 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException + + +def testShouldReturnNullWhenGettingTheValueOfAnAttributeThatIsNotListed(driver, pages): + pages.load("simpleTest.html") + head = driver.find_element_by_xpath("/html") + attribute = head.get_attribute("cheese") + assert attribute is None + + +def testShouldReturnNullWhenGettingSrcAttributeOfInvalidImgTag(driver, pages): + pages.load("simpleTest.html") + img = driver.find_element_by_id("invalidImgTag") + img_attr = img.get_attribute("src") + assert img_attr is None + + +def testShouldReturnAnAbsoluteUrlWhenGettingSrcAttributeOfAValidImgTag(driver, pages): + pages.load("simpleTest.html") + img = driver.find_element_by_id("validImgTag") + img_attr = img.get_attribute("src") + assert "icon.gif" in img_attr + + +def testShouldReturnAnAbsoluteUrlWhenGettingHrefAttributeOfAValidAnchorTag(driver, pages): + pages.load("simpleTest.html") + img = driver.find_element_by_id("validAnchorTag") + img_attr = img.get_attribute("href") + assert "icon.gif" in img_attr + + +def testShouldReturnEmptyAttributeValuesWhenPresentAndTheValueIsActuallyEmpty(driver, pages): + pages.load("simpleTest.html") + body = driver.find_element_by_xpath("//body") + assert "" == body.get_attribute("style") + + +def testShouldReturnTheValueOfTheDisabledAttributeAsFalseIfNotSet(driver, pages): + pages.load("formPage.html") + inputElement = driver.find_element_by_xpath("//input[@id='working']") + assert inputElement.get_attribute("disabled") is None + assert inputElement.is_enabled() + + pElement = driver.find_element_by_id("peas") + assert pElement.get_attribute("disabled") is None + assert pElement.is_enabled() + + +def testShouldReturnTheValueOfTheIndexAttrbuteEvenIfItIsMissing(driver, pages): + pages.load("formPage.html") + multiSelect = driver.find_element_by_id("multi") + options = multiSelect.find_elements_by_tag_name("option") + assert "1" == options[1].get_attribute("index") + + +def testShouldIndicateTheElementsThatAreDisabledAreNotis_enabled(driver, pages): + pages.load("formPage.html") + inputElement = driver.find_element_by_xpath("//input[@id='notWorking']") + assert not inputElement.is_enabled() + + inputElement = driver.find_element_by_xpath("//input[@id='working']") + assert inputElement.is_enabled() + + +def testElementsShouldBeDisabledIfTheyAreDisabledUsingRandomDisabledStrings(driver, pages): + pages.load("formPage.html") + disabledTextElement1 = driver.find_element_by_id("disabledTextElement1") + assert not disabledTextElement1.is_enabled() + + disabledTextElement2 = driver.find_element_by_id("disabledTextElement2") + assert not disabledTextElement2.is_enabled() + + disabledSubmitElement = driver.find_element_by_id("disabledSubmitElement") + assert not disabledSubmitElement.is_enabled() + + +def testShouldIndicateWhenATextAreaIsDisabled(driver, pages): + pages.load("formPage.html") + textArea = driver.find_element_by_xpath("//textarea[@id='notWorkingArea']") + assert not textArea.is_enabled() + + +def testShouldThrowExceptionIfSendingKeysToElementDisabledUsingRandomDisabledStrings(driver, pages): + pages.load("formPage.html") + disabledTextElement1 = driver.find_element_by_id("disabledTextElement1") + with pytest.raises(WebDriverException): + disabledTextElement1.send_keys("foo") + assert "" == disabledTextElement1.text + + disabledTextElement2 = driver.find_element_by_id("disabledTextElement2") + with pytest.raises(WebDriverException): + disabledTextElement2.send_keys("bar") + assert "" == disabledTextElement2.text + + +def testShouldIndicateWhenASelectIsDisabled(driver, pages): + pages.load("formPage.html") + enabled = driver.find_element_by_name("selectomatic") + disabled = driver.find_element_by_name("no-select") + + assert enabled.is_enabled() + assert not disabled.is_enabled() + + +def testShouldReturnTheValueOfCheckedForACheckboxEvenIfItLacksThatAttribute(driver, pages): + pages.load("formPage.html") + checkbox = driver.find_element_by_xpath("//input[@id='checky']") + assert checkbox.get_attribute("checked") is None + checkbox.click() + assert "true" == checkbox.get_attribute("checked") + + +def testShouldReturnTheValueOfSelectedForRadioButtonsEvenIfTheyLackThatAttribute(driver, pages): + pages.load("formPage.html") + neverSelected = driver.find_element_by_id("cheese") + initiallyNotSelected = driver.find_element_by_id("peas") + initiallySelected = driver.find_element_by_id("cheese_and_peas") + + assert neverSelected.get_attribute("checked") is None + assert initiallyNotSelected.get_attribute("checked") is None + assert "true" == initiallySelected.get_attribute("checked") + + initiallyNotSelected.click() + assert neverSelected.get_attribute("selected") is None + assert "true" == initiallyNotSelected.get_attribute("checked") + assert initiallySelected.get_attribute("checked") is None + + +def testShouldReturnTheValueOfSelectedForOptionsInSelectsEvenIfTheyLackThatAttribute(driver, pages): + pages.load("formPage.html") + selectBox = driver.find_element_by_xpath("//select[@name='selectomatic']") + options = selectBox.find_elements_by_tag_name("option") + one = options[0] + two = options[1] + assert one.is_selected() + assert not two.is_selected() + assert "true" == one.get_attribute("selected") + assert two.get_attribute("selected") is None + + +def testShouldReturnValueOfClassAttributeOfAnElement(driver, pages): + pages.load("xhtmlTest.html") + heading = driver.find_element_by_xpath("//h1") + classname = heading.get_attribute("class") + assert "header" == classname + +# Disabled due to issues with Frames +# def testShouldReturnValueOfClassAttributeOfAnElementAfterSwitchingIFrame(driver, pages): +# pages.load("iframes.html") +# driver.switch_to.frame("iframe1") +# +# wallace = driver.find_element_by_xpath("//div[@id='wallace']") +# classname = wallace.get_attribute("class") +# assert "gromit" == classname + + +def testShouldReturnTheContentsOfATextAreaAsItsValue(driver, pages): + pages.load("formPage.html") + value = driver.find_element_by_id("withText").get_attribute("value") + assert "Example text" == value + + +def testShouldReturnTheContentsOfATextAreaAsItsValueWhenSetToNonNorminalTrue(driver, pages): + pages.load("formPage.html") + e = driver.find_element_by_id("withText") + driver.execute_script("arguments[0].value = 'tRuE'", e) + value = e.get_attribute("value") + assert "tRuE" == value + + +def testShouldTreatReadonlyAsAValue(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_name("readonly") + readOnlyAttribute = element.get_attribute("readonly") + + textInput = driver.find_element_by_name("x") + notReadOnly = textInput.get_attribute("readonly") + + assert readOnlyAttribute != notReadOnly + + +def testShouldGetNumericAtribute(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("withText") + assert "5" == element.get_attribute("rows") + + +def testCanReturnATextApproximationOfTheStyleAttribute(driver, pages): + pages.load("javascriptPage.html") + style = driver.find_element_by_id("red-item").get_attribute("style") + assert "background-color" in style.lower() + + +def testShouldCorrectlyReportValueOfColspan(driver, pages): + pages.load("tables.html") + + th1 = driver.find_element_by_id("th1") + td2 = driver.find_element_by_id("td2") + + assert "th1" == th1.get_attribute("id") + assert "3" == th1.get_attribute("colspan") + + assert "td2" == td2.get_attribute("id") + assert "2" == td2.get_attribute("colspan") + + +def testCanRetrieveTheCurrentValueOfATextFormField_textInput(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("working") + assert "" == element.get_attribute("value") + element.send_keys("hello world") + assert "hello world" == element.get_attribute("value") + + +def testCanRetrieveTheCurrentValueOfATextFormField_emailInput(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("email") + assert "" == element.get_attribute("value") + element.send_keys("hello@example.com") + assert "hello@example.com" == element.get_attribute("value") + + +def testCanRetrieveTheCurrentValueOfATextFormField_textArea(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("emptyTextArea") + assert "" == element.get_attribute("value") + element.send_keys("hello world") + assert "hello world" == element.get_attribute("value") + + +def testShouldReturnNullForNonPresentBooleanAttributes(driver, pages): + pages.load("booleanAttributes.html") + element1 = driver.find_element_by_id("working") + assert element1.get_attribute("required") is None + + +@pytest.mark.xfail_ie +def testShouldReturnTrueForPresentBooleanAttributes(driver, pages): + pages.load("booleanAttributes.html") + element1 = driver.find_element_by_id("emailRequired") + assert "true" == element1.get_attribute("required") + element2 = driver.find_element_by_id("emptyTextAreaRequired") + assert "true" == element2.get_attribute("required") + element3 = driver.find_element_by_id("inputRequired") + assert "true" == element3.get_attribute("required") + element4 = driver.find_element_by_id("textAreaRequired") + assert "true" == element4.get_attribute("required") + + +def tesShouldGetUnicodeCharsFromAttribute(driver, pages): + pages.load("formPage.html") + title = driver.find_element_by_id("vsearchGadget").get_attribute("title") + assert 'Hvad s\xf8ger du?' == title diff --git a/test/selenium/webdriver/common/element_equality_tests.py b/test/selenium/webdriver/common/element_equality_tests.py new file mode 100644 index 00000000..da8b3343 --- /dev/null +++ b/test/selenium/webdriver/common/element_equality_tests.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.by import By + + +def testSameElementLookedUpDifferentWaysShouldBeEqual(driver, pages): + pages.load("simpleTest.html") + body = driver.find_element(By.TAG_NAME, "body") + xbody = driver.find_elements(By.XPATH, "//body")[0] + + assert body == xbody + + +def testDifferentElementsAreNotEqual(driver, pages): + pages.load("simpleTest.html") + body = driver.find_element(By.TAG_NAME, "body") + div = driver.find_element(By.TAG_NAME, "div") + + assert body != div + + +def testSameElementsFoundDifferentWaysShouldNotBeDuplicatedInASet(driver, pages): + pages.load("simpleTest.html") + body = driver.find_element(By.TAG_NAME, "body") + xbody = driver.find_elements(By.XPATH, "//body") + s = set(xbody) + s.add(body) + assert 1 == len(s) diff --git a/test/selenium/webdriver/common/example2.py b/test/selenium/webdriver/common/example2.py new file mode 100644 index 00000000..33b53dd3 --- /dev/null +++ b/test/selenium/webdriver/common/example2.py @@ -0,0 +1,28 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from google_one_box import GoogleOneBox + + +def testSearch(driver): + """This example shows how to use the page object pattern. + + For more information about this pattern, see: + https://github.com/SeleniumHQ/selenium/wiki/PageObjects""" + google = GoogleOneBox(driver, "http://www.google.com") + res = google.search_for("cheese") + assert res.link_contains_match_for("Wikipedia") diff --git a/test/selenium/webdriver/common/executing_async_javascript_tests.py b/test/selenium/webdriver/common/executing_async_javascript_tests.py new file mode 100644 index 00000000..5d960834 --- /dev/null +++ b/test/selenium/webdriver/common/executing_async_javascript_tests.py @@ -0,0 +1,213 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By +from selenium.common.exceptions import WebDriverException +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.remote.webelement import WebElement + + +@pytest.fixture(autouse=True) +def reset_timeouts(driver): + driver.set_script_timeout(0) + yield + driver.set_script_timeout(30) + + +def testShouldNotTimeoutIfCallbackInvokedImmediately(driver, pages): + pages.load("ajaxy_page.html") + result = driver.execute_async_script("arguments[arguments.length - 1](123);") + assert type(result) == int + assert 123 == result + + +def testShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NeitherNoneNorUndefined(driver, pages): + pages.load("ajaxy_page.html") + assert 123 == driver.execute_async_script("arguments[arguments.length - 1](123);") + assert "abc" == driver.execute_async_script("arguments[arguments.length - 1]('abc');") + assert not bool(driver.execute_async_script("arguments[arguments.length - 1](false);")) + assert bool(driver.execute_async_script("arguments[arguments.length - 1](true);")) + + +def testShouldBeAbleToReturnJavascriptPrimitivesFromAsyncScripts_NullAndUndefined(driver, pages): + pages.load("ajaxy_page.html") + assert driver.execute_async_script("arguments[arguments.length - 1](null)") is None + assert driver.execute_async_script("arguments[arguments.length - 1]()") is None + + +def testShouldBeAbleToReturnAnArrayLiteralFromAnAsyncScript(driver, pages): + pages.load("ajaxy_page.html") + result = driver.execute_async_script("arguments[arguments.length - 1]([]);") + assert "Expected not to be null!", result is not None + assert type(result) == list + assert len(result) == 0 + + +def testShouldBeAbleToReturnAnArrayObjectFromAnAsyncScript(driver, pages): + pages.load("ajaxy_page.html") + result = driver.execute_async_script("arguments[arguments.length - 1](new Array());") + assert "Expected not to be null!", result is not None + assert type(result) == list + assert len(result) == 0 + + +def testShouldBeAbleToReturnArraysOfPrimitivesFromAsyncScripts(driver, pages): + pages.load("ajaxy_page.html") + + result = driver.execute_async_script( + "arguments[arguments.length - 1]([null, 123, 'abc', true, false]);") + + assert result is not None + assert type(result) == list + assert not bool(result.pop()) + assert bool(result.pop()) + assert "abc" == result.pop() + assert 123 == result.pop() + assert result.pop() is None + assert len(result) == 0 + + +def testShouldBeAbleToReturnWebElementsFromAsyncScripts(driver, pages): + pages.load("ajaxy_page.html") + + result = driver.execute_async_script("arguments[arguments.length - 1](document.body);") + assert isinstance(result, WebElement) + assert "body" == result.tag_name.lower() + + +def testShouldBeAbleToReturnArraysOfWebElementsFromAsyncScripts(driver, pages): + pages.load("ajaxy_page.html") + + result = driver.execute_async_script( + "arguments[arguments.length - 1]([document.body, document.body]);") + assert result is not None + assert type(result) == list + + list_ = result + assert 2 == len(list_) + assert isinstance(list_[0], WebElement) + assert isinstance(list_[1], WebElement) + assert "body" == list_[0].tag_name + # assert list_[0] == list_[1] + + +def testShouldTimeoutIfScriptDoesNotInvokeCallback(driver, pages): + pages.load("ajaxy_page.html") + with pytest.raises(TimeoutException): + # Script is expected to be async and explicitly callback, so this should timeout. + driver.execute_async_script("return 1 + 2;") + + +def testShouldTimeoutIfScriptDoesNotInvokeCallbackWithAZeroTimeout(driver, pages): + pages.load("ajaxy_page.html") + with pytest.raises(TimeoutException): + driver.execute_async_script("window.setTimeout(function() {}, 0);") + + +@pytest.mark.xfail_marionette +@pytest.mark.xfail_remote +def testShouldNotTimeoutIfScriptCallsbackInsideAZeroTimeout(driver, pages): + pages.load("ajaxy_page.html") + driver.execute_async_script( + """var callback = arguments[arguments.length - 1]; + window.setTimeout(function() { callback(123); }, 0)""") + + +def testShouldTimeoutIfScriptDoesNotInvokeCallbackWithLongTimeout(driver, pages): + driver.set_script_timeout(0.5) + pages.load("ajaxy_page.html") + with pytest.raises(TimeoutException): + driver.execute_async_script( + """var callback = arguments[arguments.length - 1]; + window.setTimeout(callback, 1500);""") + + +def testShouldDetectPageLoadsWhileWaitingOnAnAsyncScriptAndReturnAnError(driver, pages): + pages.load("ajaxy_page.html") + driver.set_script_timeout(0.1) + with pytest.raises(WebDriverException): + url = pages.url("dynamic.html") + driver.execute_async_script("window.location = '{0}';".format(url)) + + +def testShouldCatchErrorsWhenExecutingInitialScript(driver, pages): + pages.load("ajaxy_page.html") + with pytest.raises(WebDriverException): + driver.execute_async_script("throw Error('you should catch this!');") + + +def testShouldBeAbleToExecuteAsynchronousScripts(driver, pages): + pages.load("ajaxy_page.html") + + typer = driver.find_element(by=By.NAME, value="typer") + typer.send_keys("bob") + assert "bob" == typer.get_attribute("value") + + driver.find_element(by=By.ID, value="red").click() + driver.find_element(by=By.NAME, value="submit").click() + + assert 1 == len(driver.find_elements(by=By.TAG_NAME, value='div')), \ + "There should only be 1 DIV at this point, which is used for the butter message" + driver.set_script_timeout(10) + text = driver.execute_async_script( + """var callback = arguments[arguments.length - 1]; + window.registerListener(arguments[arguments.length - 1]);""") + assert "bob" == text + assert "" == typer.get_attribute("value") + + assert 2 == len(driver.find_elements(by=By.TAG_NAME, value='div')), \ + "There should be 1 DIV (for the butter message) + 1 DIV (for the new label)" + + +def testShouldBeAbleToPassMultipleArgumentsToAsyncScripts(driver, pages): + pages.load("ajaxy_page.html") + result = driver.execute_async_script(""" + arguments[arguments.length - 1](arguments[0] + arguments[1]);""", 1, 2) + assert 3 == result + +# TODO DavidBurns Disabled till Java WebServer is used +# def testShouldBeAbleToMakeXMLHttpRequestsAndWaitForTheResponse(driver, pages): +# script = """ +# var url = arguments[0]; +# var callback = arguments[arguments.length - 1]; +# // Adapted from http://www.quirksmode.org/js/xmlhttp.html +# var XMLHttpFactories = [ +# function () return new XMLHttpRequest(), +# function () return new ActiveXObject('Msxml2.XMLHTTP'), +# function () return new ActiveXObject('Msxml3.XMLHTTP'), +# function () return new ActiveXObject('Microsoft.XMLHTTP') +# ]; +# var xhr = false; +# while (!xhr && XMLHttpFactories.length) +# try{ +# xhr = XMLHttpFactories.shift().call(); +# }catch (e) +# +# if (!xhr) throw Error('unable to create XHR object'); +# xhr.open('GET', url, true); +# xhr.onreadystatechange = function() +# if (xhr.readyState == 4) callback(xhr.responseText); +# +# xhr.send('');""" # empty string to stop firefox 3 from choking +# +# pages.load("ajaxy_page.html") +# driver.set_script_timeout(3) +# response = driver.execute_async_script(script, pages.sleepingPage + "?time=2") +# htm = "<html><head><title>Done</title></head><body>Slept for 2s</body></html>" +# assert response.strip() == htm diff --git a/test/selenium/webdriver/common/executing_javascript_tests.py b/test/selenium/webdriver/common/executing_javascript_tests.py new file mode 100644 index 00000000..aa19183d --- /dev/null +++ b/test/selenium/webdriver/common/executing_javascript_tests.py @@ -0,0 +1,282 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.remote.webelement import WebElement + +try: + str = unicode +except NameError: + pass + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAString(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return document.title") + + assert type(result) == str, "The type of the result is %s" % type(result) + assert "XHTML Test Page" == result + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAnInteger(driver, pages): + pages.load("nestedElements.html") + result = driver.execute_script("return document.getElementsByName('checky').length") + + assert type(result) == int + assert int(result) > 1 + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAWebElement(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return document.getElementById('id1')") + + assert result is not None + assert isinstance(result, WebElement) + assert "a" == result.tag_name.lower() + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAListOfWebElements(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return document.querySelectorAll('div.navigation a')") + + assert result is not None + assert isinstance(result, list) + assert all(isinstance(item, WebElement) for item in result) + assert all('a' == item.tag_name.lower() for item in result) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideAList(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return [document.body]") + + assert result is not None + assert isinstance(result, list) + assert isinstance(result[0], WebElement) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideANestedList(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return [document.body, [document.getElementById('id1')]]") + + assert result is not None + assert isinstance(result, list) + assert isinstance(result[0], WebElement) + assert isinstance(result[1][0], WebElement) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideADict(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return {el1: document.body}") + + assert result is not None + assert isinstance(result, dict) + assert isinstance(result.get('el1'), WebElement) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideANestedDict(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return {el1: document.body, " + "nested: {el2: document.getElementById('id1')}}") + + assert result is not None + assert isinstance(result, dict) + assert isinstance(result.get('el1'), WebElement) + assert isinstance(result.get('nested').get('el2'), WebElement) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnWebElementsInsideAListInsideADict(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return {el1: [document.body]}") + + assert result is not None + assert isinstance(result, dict) + assert isinstance(result.get('el1'), list) + assert isinstance(result.get('el1')[0], WebElement) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnABoolean(driver, pages): + pages.load("xhtmlTest.html") + + result = driver.execute_script("return true") + + assert result is not None + assert type(result) == bool + assert bool(result) + + +def testShouldBeAbleToExecuteSimpleJavascriptAndAStringsArray(driver, pages): + pages.load("javascriptPage.html") + expectedResult = [] + expectedResult.append("zero") + expectedResult.append("one") + expectedResult.append("two") + result = driver.execute_script( + "return ['zero', 'one', 'two']") + + assert expectedResult == result + + +def testShouldBeAbleToExecuteSimpleJavascriptAndReturnAnArray(driver, pages): + pages.load("javascriptPage.html") + expectedResult = [] + expectedResult.append("zero") + subList = [] + subList.append(True) + subList.append(False) + expectedResult.append(subList) + result = driver.execute_script("return ['zero', [true, false]]") + assert result is not None + assert type(result) == list + assert expectedResult == result + + +def testPassingAndReturningAnIntShouldReturnAWholeNumber(driver, pages): + pages.load("javascriptPage.html") + expectedResult = 1 + result = driver.execute_script("return arguments[0]", expectedResult) + assert type(result) == int + assert expectedResult == result + + +def testPassingAndReturningADoubleShouldReturnADecimal(driver, pages): + pages.load("javascriptPage.html") + expectedResult = 1.2 + result = driver.execute_script("return arguments[0]", expectedResult) + assert type(result) == float + assert expectedResult == result + + +def testShouldThrowAnExceptionWhenTheJavascriptIsBad(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(WebDriverException): + driver.execute_script("return squiggle()") + + +def testShouldBeAbleToCallFunctionsDefinedOnThePage(driver, pages): + pages.load("javascriptPage.html") + driver.execute_script("displayMessage('I like cheese')") + text = driver.find_element_by_id("result").text + assert "I like cheese" == text.strip() + + +def testShouldBeAbleToPassAStringAnAsArgument(driver, pages): + pages.load("javascriptPage.html") + value = driver.execute_script( + "return arguments[0] == 'fish' ? 'fish' : 'not fish'", "fish") + assert "fish" == value + + +def testShouldBeAbleToPassABooleanAnAsArgument(driver, pages): + pages.load("javascriptPage.html") + value = bool(driver.execute_script("return arguments[0] == true", True)) + assert value + + +def testShouldBeAbleToPassANumberAnAsArgument(driver, pages): + pages.load("javascriptPage.html") + value = bool(driver.execute_script("return arguments[0] == 1 ? true : false", 1)) + assert value + + +def testShouldBeAbleToPassAWebElementAsArgument(driver, pages): + pages.load("javascriptPage.html") + button = driver.find_element_by_id("plainButton") + value = driver.execute_script( + "arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble']", + button) + assert "plainButton" == value + + +def testShouldBeAbleToPassAnArrayAsArgument(driver, pages): + pages.load("javascriptPage.html") + array = ["zero", 1, True, 3.14159] + length = int(driver.execute_script("return arguments[0].length", array)) + assert len(array) == length + + +def testShouldBeAbleToPassACollectionAsArgument(driver, pages): + pages.load("javascriptPage.html") + collection = [] + collection.append("Cheddar") + collection.append("Brie") + collection.append(7) + length = int(driver.execute_script("return arguments[0].length", collection)) + assert len(collection) == length + + collection = [] + collection.append("Gouda") + collection.append("Stilton") + collection.append("Stilton") + collection.append(True) + length = int(driver.execute_script("return arguments[0].length", collection)) + assert len(collection) == length + + +def testShouldThrowAnExceptionIfAnArgumentIsNotValid(driver, pages): + pages.load("javascriptPage.html") + with pytest.raises(Exception): + driver.execute_script("return arguments[0]", driver) + + +def testShouldBeAbleToPassInMoreThanOneArgument(driver, pages): + pages.load("javascriptPage.html") + result = driver.execute_script("return arguments[0] + arguments[1]", "one", "two") + assert "onetwo" == result + + +def testJavascriptStringHandlingShouldWorkAsExpected(driver, pages): + pages.load("javascriptPage.html") + value = driver.execute_script("return ''") + assert "" == value + + value = driver.execute_script("return undefined") + assert value is None + + value = driver.execute_script("return ' '") + assert " " == value + + +def testShouldBeAbleToCreateAPersistentValue(driver, pages): + pages.load("formPage.html") + driver.execute_script("document.alerts = []") + driver.execute_script("document.alerts.push('hello world')") + text = driver.execute_script("return document.alerts.shift()") + assert "hello world" == text + + +def testCanPassADictionaryAsAParameter(driver, pages): + pages.load("simpleTest.html") + nums = [1, 2] + args = {"bar": "test", "foo": nums} + res = driver.execute_script("return arguments[0]['foo'][1]", args) + assert 2 == res + + +def testCanPassANone(driver, pages): + pages.load("simpleTest.html") + res = driver.execute_script("return arguments[0] === null", None) + assert res diff --git a/test/selenium/webdriver/common/form_handling_tests.py b/test/selenium/webdriver/common/form_handling_tests.py new file mode 100644 index 00000000..deeacb9c --- /dev/null +++ b/test/selenium/webdriver/common/form_handling_tests.py @@ -0,0 +1,235 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import NoSuchElementException +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +def testShouldClickOnSubmitInputElements(driver, pages): + pages.load("formPage.html") + driver.find_element_by_id("submitButton").click() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + +def testClickingOnUnclickableElementsDoesNothing(driver, pages): + pages.load("formPage.html") + driver.find_element_by_xpath("//body").click() + + +def testShouldBeAbleToClickImageButtons(driver, pages): + pages.load("formPage.html") + driver.find_element_by_id("imageButton").click() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + +def testShouldBeAbleToSubmitForms(driver, pages): + pages.load("formPage.html") + driver.find_element_by_name("login").submit() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + +def testShouldSubmitAFormWhenAnyInputElementWithinThatFormIsSubmitted(driver, pages): + pages.load("formPage.html") + driver.find_element_by_id("checky").submit() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + +def testShouldSubmitAFormWhenAnyElementWihinThatFormIsSubmitted(driver, pages): + pages.load("formPage.html") + driver.find_element_by_xpath("//form/p").submit() + WebDriverWait(driver, 5).until(EC.title_is("We Arrive Here")) + + +def testShouldNotBeAbleToSubmitAFormThatDoesNotExist(driver, pages): + pages.load("formPage.html") + with pytest.raises(NoSuchElementException): + driver.find_element_by_name("there is no spoon").submit() + + +def testShouldBeAbleToEnterTextIntoATextAreaBySettingItsValue(driver, pages): + pages.load("javascriptPage.html") + textarea = driver.find_element_by_id("keyUpArea") + cheesey = "Brie and cheddar" + textarea.send_keys(cheesey) + assert textarea.get_attribute("value") == cheesey + + +def testShouldEnterDataIntoFormFields(driver, pages): + pages.load("xhtmlTest.html") + element = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']") + originalValue = element.get_attribute("value") + assert originalValue == "change" + + element.clear() + element.send_keys("some text") + + element = driver.find_element_by_xpath("//form[@name='someForm']/input[@id='username']") + newFormValue = element.get_attribute("value") + assert newFormValue == "some text" + + +def testShouldBeAbleToSelectACheckBox(driver, pages): + pages.load("formPage.html") + checkbox = driver.find_element_by_id("checky") + assert checkbox.is_selected() is False + checkbox.click() + assert checkbox.is_selected() is True + checkbox.click() + assert checkbox.is_selected() is False + + +def testShouldToggleTheCheckedStateOfACheckbox(driver, pages): + pages.load("formPage.html") + checkbox = driver.find_element_by_id("checky") + assert checkbox.is_selected() is False + checkbox.click() + assert checkbox.is_selected() is True + checkbox.click() + assert checkbox.is_selected() is False + + +def testTogglingACheckboxShouldReturnItsCurrentState(driver, pages): + pages.load("formPage.html") + checkbox = driver.find_element_by_id("checky") + assert checkbox.is_selected() is False + checkbox.click() + assert checkbox.is_selected() is True + checkbox.click() + assert checkbox.is_selected() is False + + +def testShouldBeAbleToSelectARadioButton(driver, pages): + pages.load("formPage.html") + radioButton = driver.find_element_by_id("peas") + assert radioButton.is_selected() is False + radioButton.click() + assert radioButton.is_selected() is True + + +def testShouldBeAbleToSelectARadioButtonByClickingOnIt(driver, pages): + pages.load("formPage.html") + radioButton = driver.find_element_by_id("peas") + assert radioButton.is_selected() is False + radioButton.click() + assert radioButton.is_selected() is True + + +def testShouldReturnStateOfRadioButtonsBeforeInteration(driver, pages): + pages.load("formPage.html") + radioButton = driver.find_element_by_id("cheese_and_peas") + assert radioButton.is_selected() is True + + radioButton = driver.find_element_by_id("cheese") + assert radioButton.is_selected() is False + +# [ExpectedException(typeof(NotImplementedException))] +# def testShouldThrowAnExceptionWhenTogglingTheStateOfARadioButton(driver, pages): +# pages.load("formPage.html") +# radioButton = driver.find_element_by_id("cheese")) +# radioButton.click() + +# [IgnoreBrowser(Browser.IE, "IE allows toggling of an option not in a multiselect")] +# [ExpectedException(typeof(NotImplementedException))] +# def testTogglingAnOptionShouldThrowAnExceptionIfTheOptionIsNotInAMultiSelect(driver, pages): +# pages.load("formPage.html") +# select = driver.find_element_by_name("selectomatic")) +# option = select.find_elements_by_tag_name("option"))[0] +# option.click() + + +def testTogglingAnOptionShouldToggleOptionsInAMultiSelect(driver, pages): + pages.load("formPage.html") + + select = driver.find_element_by_name("multi") + option = select.find_elements_by_tag_name("option")[0] + + selected = option.is_selected() + option.click() + assert not selected == option.is_selected() + + option.click() + assert selected == option.is_selected() + + +def testShouldThrowAnExceptionWhenSelectingAnUnselectableElement(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_xpath("//title") + with pytest.raises(WebDriverException): + element.click() + + +def testSendingKeyboardEventsShouldAppendTextInInputs(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("working") + element.send_keys("Some") + value = element.get_attribute("value") + assert value == "Some" + + element.send_keys(" text") + value = element.get_attribute("value") + assert value == "Some text" + + +def testShouldBeAbleToClearTextFromInputElements(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("working") + element.send_keys("Some text") + value = element.get_attribute("value") + assert len(value) > 0 + + element.clear() + value = element.get_attribute("value") + assert len(value) == 0 + + +def testEmptyTextBoxesShouldReturnAnEmptyStringNotNull(driver, pages): + pages.load("formPage.html") + emptyTextBox = driver.find_element_by_id("working") + assert emptyTextBox.get_attribute("value") == "" + + emptyTextArea = driver.find_element_by_id("emptyTextArea") + assert emptyTextArea.get_attribute("value") == "" + + +def testShouldBeAbleToClearTextFromTextAreas(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id("withText") + element.send_keys("Some text") + value = element.get_attribute("value") + assert len(value) > 0 + + element.clear() + value = element.get_attribute("value") + assert len(value) == 0 + + +def testRadioShouldNotBeSelectedAfterSelectingSibling(driver, pages): + pages.load("formPage.html") + cheese = driver.find_element_by_id("cheese") + peas = driver.find_element_by_id("peas") + + cheese.click() + assert cheese.is_selected() is True + assert peas.is_selected() is False + + peas.click() + assert cheese.is_selected() is False + assert peas.is_selected() is True diff --git a/test/selenium/webdriver/common/frame_switching_tests.py b/test/selenium/webdriver/common/frame_switching_tests.py new file mode 100644 index 00000000..4c7c9a62 --- /dev/null +++ b/test/selenium/webdriver/common/frame_switching_tests.py @@ -0,0 +1,427 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import ( + NoSuchElementException, + NoSuchFrameException, + WebDriverException) +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +# ---------------------------------------------------------------------------------------------- +# +# Tests that WebDriver doesn't do anything fishy when it navigates to a page with frames. +# +# ---------------------------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +def restore_default_context(driver): + yield + driver.switch_to.default_content() + + +def testShouldAlwaysFocusOnTheTopMostFrameAfterANavigationEvent(driver, pages): + pages.load("frameset.html") + driver.find_element(By.TAG_NAME, "frameset") # Test passes if this does not throw. + + +def testShouldNotAutomaticallySwitchFocusToAnIFrameWhenAPageContainingThemIsLoaded(driver, pages): + pages.load("iframes.html") + driver.find_element(By.ID, "iframe_page_heading") + + +def testShouldOpenPageWithBrokenFrameset(driver, pages): + pages.load("framesetPage3.html") + + frame1 = driver.find_element(By.ID, "first") + driver.switch_to.frame(frame1) + driver.switch_to.default_content() + + frame2 = driver.find_element(By.ID, "second") + driver.switch_to.frame(frame2) # IE9 can not switch to this broken frame - it has no window. + +# ---------------------------------------------------------------------------------------------- +# +# Tests that WebDriver can switch to frames as expected. +# +# ---------------------------------------------------------------------------------------------- + + +def testShouldBeAbleToSwitchToAFrameByItsIndex(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(1) + assert driver.find_element(By.ID, "pageNumber").text == "2" + + +def testShouldBeAbleToSwitchToAnIframeByItsIndex(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame(0) + assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name" + + +def testShouldBeAbleToSwitchToAFrameByItsName(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame("fourth") + assert driver.find_element(By.TAG_NAME, "frame").get_attribute("name") == "child1" + + +def testShouldBeAbleToSwitchToAnIframeByItsName(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame("iframe1-name") + assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name" + + +def testShouldBeAbleToSwitchToAFrameByItsID(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame("fifth") + assert driver.find_element(By.NAME, "windowOne").text == "Open new window" + + +def testShouldBeAbleToSwitchToAnIframeByItsID(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame("iframe1") + assert driver.find_element(By.NAME, "id-name1").get_attribute("value") == "name" + + +def testShouldBeAbleToSwitchToFrameWithNameContainingDot(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame("sixth.iframe1") + assert "Page number 3" in driver.find_element(By.TAG_NAME, "body").text + + +def testShouldBeAbleToSwitchToAFrameUsingAPreviouslyLocatedWebElement(driver, pages): + pages.load("frameset.html") + frame = driver.find_element(By.TAG_NAME, "frame") + driver.switch_to.frame(frame) + assert driver.find_element(By.ID, "pageNumber").text == "1" + + +def testShouldBeAbleToSwitchToAnIFrameUsingAPreviouslyLocatedWebElement(driver, pages): + pages.load("iframes.html") + frame = driver.find_element(By.TAG_NAME, "iframe") + driver.switch_to.frame(frame) + + element = driver.find_element(By.NAME, "id-name1") + assert element.get_attribute("value") == "name" + + +def testShouldEnsureElementIsAFrameBeforeSwitching(driver, pages): + pages.load("frameset.html") + frame = driver.find_element(By.TAG_NAME, "frameset") + with pytest.raises(NoSuchFrameException): + driver.switch_to.frame(frame) + + +def testFrameSearchesShouldBeRelativeToTheCurrentlySelectedFrame(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame("second") + assert driver.find_element(By.ID, "pageNumber").text == "2" + + with pytest.raises(NoSuchElementException): + driver.switch_to.frame(driver.find_element_by_name("third")) + + driver.switch_to.default_content() + driver.switch_to.frame(driver.find_element_by_name("third")) + + with pytest.raises(NoSuchFrameException): + driver.switch_to.frame("second") + + driver.switch_to.default_content() + driver.switch_to.frame(driver.find_element_by_name("second")) + assert driver.find_element(By.ID, "pageNumber").text == "2" + + +def testShouldSelectChildFramesByChainedCalls(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("fourth")) + driver.switch_to.frame(driver.find_element_by_name("child2")) + assert driver.find_element(By.ID, "pageNumber").text == "11" + + +def testShouldThrowFrameNotFoundExceptionLookingUpSubFramesWithSuperFrameNames(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("fourth")) + with pytest.raises(NoSuchElementException): + driver.switch_to.frame(driver.find_element_by_name("second")) + + +def testShouldThrowAnExceptionWhenAFrameCannotBeFound(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchElementException): + driver.switch_to.frame(driver.find_element_by_name("Nothing here")) + + +def testShouldThrowAnExceptionWhenAFrameCannotBeFoundByIndex(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchFrameException): + driver.switch_to.frame(27) + + +def testShouldBeAbleToSwitchToParentFrame(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("fourth")) + driver.switch_to.parent_frame() + driver.switch_to.frame(driver.find_element_by_name("first")) + assert driver.find_element(By.ID, "pageNumber").text == "1" + + +def testShouldBeAbleToSwitchToParentFrameFromASecondLevelFrame(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("fourth")) + driver.switch_to.frame(driver.find_element_by_name("child1")) + driver.switch_to.parent_frame() + driver.switch_to.frame(driver.find_element_by_name("child2")) + assert driver.find_element(By.ID, "pageNumber").text == "11" + + +def testSwitchingToParentFrameFromDefaultContextIsNoOp(driver, pages): + pages.load("xhtmlTest.html") + driver.switch_to.parent_frame() + assert driver.title == "XHTML Test Page" + + +def testShouldBeAbleToSwitchToParentFromAnIframe(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame(0) + driver.switch_to.parent_frame() + driver.find_element(By.ID, "iframe_page_heading") + +# ---------------------------------------------------------------------------------------------- +# +# General frame handling behavior tests +# +# ---------------------------------------------------------------------------------------------- + + +def testShouldContinueToReferToTheSameFrameOnceItHasBeenSelected(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(2) + checkbox = driver.find_element(By.XPATH, "//input[@name='checky']") + checkbox.click() + checkbox.submit() + + # TODO(simon): this should not be needed, and is only here because IE's submit returns too + # soon. + + WebDriverWait(driver, 3).until(EC.text_to_be_present_in_element((By.XPATH, '//p'), 'Success!')) + + +@pytest.mark.xfail_marionette(raises=WebDriverException, + reason='https://github.com/mozilla/geckodriver/issues/610') +@pytest.mark.xfail_remote(raises=WebDriverException, + reason='https://github.com/mozilla/geckodriver/issues/610') +def testShouldFocusOnTheReplacementWhenAFrameFollowsALinkToA_TopTargetedPage(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(0) + driver.find_element(By.LINK_TEXT, "top").click() + + expectedTitle = "XHTML Test Page" + + WebDriverWait(driver, 3).until(EC.title_is(expectedTitle)) + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "only-exists-on-xhtmltest"))) + + +def testShouldAllowAUserToSwitchFromAnIframeBackToTheMainContentOfThePage(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame(0) + driver.switch_to.default_content() + driver.find_element(By.ID, "iframe_page_heading") + + +def testShouldAllowTheUserToSwitchToAnIFrameAndRemainFocusedOnIt(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame(0) + driver.find_element(By.ID, "submitButton").click() + assert getTextOfGreetingElement(driver) == "Success!" + + +def getTextOfGreetingElement(driver): + return WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "greeting"))).text + + +def testShouldBeAbleToClickInAFrame(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame("third") + + # This should replace frame "third" ... + driver.find_element(By.ID, "submitButton").click() + # driver should still be focused on frame "third" ... + assert getTextOfGreetingElement(driver) == "Success!" + # Make sure it was really frame "third" which was replaced ... + driver.switch_to.default_content() + driver.switch_to.frame("third") + assert getTextOfGreetingElement(driver) == "Success!" + + +def testShouldBeAbleToClickInAFrameThatRewritesTopWindowLocation(driver, pages): + pages.load("click_tests/issue5237.html") + driver.switch_to.frame(driver.find_element_by_id("search")) + driver.find_element(By.ID, "submit").click() + driver.switch_to.default_content() + WebDriverWait(driver, 3).until(EC.title_is("Target page for issue 5237")) + + +def testShouldBeAbleToClickInASubFrame(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_id("sixth")) + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + + # This should replace frame "iframe1" inside frame "sixth" ... + driver.find_element(By.ID, "submitButton").click() + # driver should still be focused on frame "iframe1" inside frame "sixth" ... + assert getTextOfGreetingElement(driver), "Success!" + # Make sure it was really frame "iframe1" inside frame "sixth" which was replaced ... + driver.switch_to.default_content() + driver.switch_to.frame(driver.find_element_by_id("sixth")) + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + assert driver.find_element(By.ID, "greeting").text == "Success!" + + +def testShouldBeAbleToFindElementsInIframesByXPath(driver, pages): + pages.load("iframes.html") + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + element = driver.find_element(By.XPATH, "//*[@id = 'changeme']") + assert element is not None + + +def testGetCurrentUrlReturnsTopLevelBrowsingContextUrl(driver, pages): + pages.load("frameset.html") + assert "frameset.html" in driver.current_url + driver.switch_to.frame(driver.find_element_by_name("second")) + assert "frameset.html" in driver.current_url + + +def testGetCurrentUrlReturnsTopLevelBrowsingContextUrlForIframes(driver, pages): + pages.load("iframes.html") + assert "iframes.html" in driver.current_url + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + assert "iframes.html" in driver.current_url + + +def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUs(driver, pages): + pages.load("frame_switching_tests/deletingFrame.html") + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + + killIframe = driver.find_element(By.ID, "killIframe") + killIframe.click() + driver.switch_to.default_content() + WebDriverWait(driver, 3).until_not( + EC.presence_of_element_located((By.ID, "iframe1"))) + + addIFrame = driver.find_element(By.ID, "addBackFrame") + addIFrame.click() + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "iframe1"))) + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success"))) + + +def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUsWithFrameIndex(driver, pages): + pages.load("frame_switching_tests/deletingFrame.html") + iframe = 0 + WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe)) + # we should be in the frame now + killIframe = driver.find_element(By.ID, "killIframe") + killIframe.click() + driver.switch_to.default_content() + + addIFrame = driver.find_element(By.ID, "addBackFrame") + addIFrame.click() + WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe)) + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success"))) + + +def testShouldBeAbleToSwitchToTheTopIfTheFrameIsDeletedFromUnderUsWithWebelement(driver, pages): + pages.load("frame_switching_tests/deletingFrame.html") + iframe = driver.find_element(By.ID, "iframe1") + WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe)) + # we should be in the frame now + killIframe = driver.find_element(By.ID, "killIframe") + killIframe.click() + driver.switch_to.default_content() + + addIFrame = driver.find_element(By.ID, "addBackFrame") + addIFrame.click() + + iframe = driver.find_element(By.ID, "iframe1") + WebDriverWait(driver, 3).until(EC.frame_to_be_available_and_switch_to_it(iframe)) + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "success"))) + + +@pytest.mark.xfail_chrome(raises=NoSuchElementException) +@pytest.mark.xfail_marionette(raises=WebDriverException, + reason='https://github.com/mozilla/geckodriver/issues/614') +@pytest.mark.xfail_remote(raises=WebDriverException, + reason='https://github.com/mozilla/geckodriver/issues/614') +@pytest.mark.xfail_webkitgtk(raises=NoSuchElementException) +def testShouldNotBeAbleToDoAnythingTheFrameIsDeletedFromUnderUs(driver, pages): + pages.load("frame_switching_tests/deletingFrame.html") + driver.switch_to.frame(driver.find_element_by_id("iframe1")) + + killIframe = driver.find_element(By.ID, "killIframe") + killIframe.click() + + with pytest.raises(NoSuchFrameException): + driver.find_element(By.ID, "killIframe").click() + + +def testShouldReturnWindowTitleInAFrameset(driver, pages): + pages.load("frameset.html") + driver.switch_to.frame(driver.find_element_by_name("third")) + assert "Unique title" == driver.title + + +def testJavaScriptShouldExecuteInTheContextOfTheCurrentFrame(driver, pages): + pages.load("frameset.html") + assert driver.execute_script("return window == window.top") + driver.switch_to.frame(driver.find_element(By.NAME, "third")) + assert driver.execute_script("return window != window.top") + + +def testShouldNotSwitchMagicallyToTheTopWindow(driver, pages): + pages.load("frame_switching_tests/bug4876.html") + driver.switch_to.frame(0) + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "inputText"))) + + for i in range(20): + try: + input = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "inputText"))) + submit = WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "submitButton"))) + input.clear() + import random + input.send_keys("rand%s" % int(random.random())) + submit.click() + finally: + url = driver.execute_script("return window.location.href") + # IE6 and Chrome add "?"-symbol to the end of the URL + if (url.endswith("?")): + url = url[:len(url) - 1] + + assert pages.url("frame_switching_tests/bug4876_iframe.html") == url + + +def testGetShouldSwitchToDefaultContext(driver, pages): + pages.load("iframes.html") + driver.find_element(By.ID, "iframe1") + driver.switch_to.frame(driver.find_element(By.ID, "iframe1")) + driver.find_element(By.ID, "cheese") # Found on formPage.html but not on iframes.html. + pages.load("iframes.html") # This must effectively switch_to.default_content(), too. + driver.find_element(By.ID, "iframe1") diff --git a/test/selenium/webdriver/common/google_one_box.py b/test/selenium/webdriver/common/google_one_box.py new file mode 100644 index 00000000..fe57a184 --- /dev/null +++ b/test/selenium/webdriver/common/google_one_box.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.common.exceptions import NoSuchElementException +from results_page import ResultsPage +from page_loader import require_loaded + + +class GoogleOneBox(object): + """This class models a page that has a google search bar.""" + + def __init__(self, driver, url): + self._driver = driver + self._url = url + + def is_loaded(self): + try: + self._driver.find_element_by_name("q") + return True + except NoSuchElementException: + return False + + def load(self): + self._driver.get(self._url) + + @require_loaded + def search_for(self, search_term): + element = self._driver.find_element_by_name("q") + element.send_keys(search_term) + element.submit() + return ResultsPage(self._driver) diff --git a/test/selenium/webdriver/common/implicit_waits_tests.py b/test/selenium/webdriver/common/implicit_waits_tests.py new file mode 100644 index 00000000..2e305252 --- /dev/null +++ b/test/selenium/webdriver/common/implicit_waits_tests.py @@ -0,0 +1,73 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import NoSuchElementException + + +def testShouldImplicitlyWaitForASingleElement(driver, pages): + pages.load("dynamic.html") + add = driver.find_element_by_id("adder") + driver.implicitly_wait(3) + add.click() + driver.find_element_by_id("box0") # All is well if this doesn't throw. + + +def testShouldStillFailToFindAnElementWhenImplicitWaitsAreEnabled(driver, pages): + pages.load("dynamic.html") + driver.implicitly_wait(0.5) + with pytest.raises(NoSuchElementException): + driver.find_element_by_id("box0") + + +def testShouldReturnAfterFirstAttemptToFindOneAfterDisablingImplicitWaits(driver, pages): + pages.load("dynamic.html") + driver.implicitly_wait(3) + driver.implicitly_wait(0) + with pytest.raises(NoSuchElementException): + driver.find_element_by_id("box0") + + +def testShouldImplicitlyWaitUntilAtLeastOneElementIsFoundWhenSearchingForMany(driver, pages): + pages.load("dynamic.html") + add = driver.find_element_by_id("adder") + + driver.implicitly_wait(2) + add.click() + add.click() + + elements = driver.find_elements_by_class_name("redbox") + assert len(elements) >= 1 + + +def testShouldStillFailToFindAnElemenstWhenImplicitWaitsAreEnabled(driver, pages): + pages.load("dynamic.html") + + driver.implicitly_wait(0.5) + elements = driver.find_elements_by_class_name("redbox") + assert 0 == len(elements) + + +def testShouldReturnAfterFirstAttemptToFindManyAfterDisablingImplicitWaits(driver, pages): + pages.load("dynamic.html") + add = driver.find_element_by_id("adder") + driver.implicitly_wait(1.1) + driver.implicitly_wait(0) + add.click() + elements = driver.find_elements_by_class_name("redbox") + assert 0 == len(elements) diff --git a/test/selenium/webdriver/common/interactions_tests.py b/test/selenium/webdriver/common/interactions_tests.py new file mode 100644 index 00000000..0a2908b2 --- /dev/null +++ b/test/selenium/webdriver/common/interactions_tests.py @@ -0,0 +1,265 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""Tests for advanced user interactions.""" +import pytest + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.support.ui import WebDriverWait + + +def performDragAndDropWithMouse(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("draggableLists.html") + dragReporter = driver.find_element_by_id("dragging_reports") + toDrag = driver.find_element_by_id("rightitem-3") + dragInto = driver.find_element_by_id("sortable1") + + holdItem = ActionChains(driver).click_and_hold(toDrag) + moveToSpecificItem = ActionChains(driver) \ + .move_to_element(driver.find_element_by_id("leftitem-4")) + moveToOtherList = ActionChains(driver).move_to_element(dragInto) + drop = ActionChains(driver).release(dragInto) + assert "Nothing happened." == dragReporter.text + + holdItem.perform() + moveToSpecificItem.perform() + moveToOtherList.perform() + assert "Nothing happened. DragOut" == dragReporter.text + + drop.perform() + + +def testDraggingElementWithMouseMovesItToAnotherList(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + performDragAndDropWithMouse(driver, pages) + dragInto = driver.find_element_by_id("sortable1") + assert 6 == len(dragInto.find_elements_by_tag_name("li")) + + +def testDraggingElementWithMouseFiresEvents(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + performDragAndDropWithMouse(driver, pages) + dragReporter = driver.find_element_by_id("dragging_reports") + assert "Nothing happened. DragOut DropIn RightItem 3" == dragReporter.text + + +def _isElementAvailable(driver, id): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + try: + driver.find_element_by_id(id) + return True + except Exception: + return False + + +def testDragAndDrop(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + element_available_timeout = 15 + wait = WebDriverWait(driver, element_available_timeout) + pages.load("droppableItems.html") + wait.until(lambda dr: _isElementAvailable(driver, "draggable")) + + if not _isElementAvailable(driver, "draggable"): + raise AssertionError("Could not find draggable element after 15 seconds.") + + toDrag = driver.find_element_by_id("draggable") + dropInto = driver.find_element_by_id("droppable") + + holdDrag = ActionChains(driver) \ + .click_and_hold(toDrag) + move = ActionChains(driver) \ + .move_to_element(dropInto) + drop = ActionChains(driver).release(dropInto) + + holdDrag.perform() + move.perform() + drop.perform() + + dropInto = driver.find_element_by_id("droppable") + text = dropInto.find_element_by_tag_name("p").text + assert "Dropped!" == text + + +def testDoubleClick(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("javascriptPage.html") + toDoubleClick = driver.find_element_by_id("doubleClickField") + + dblClick = ActionChains(driver) \ + .double_click(toDoubleClick) + + dblClick.perform() + assert "DoubleClicked" == toDoubleClick.get_attribute('value') + + +def testContextClick(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("javascriptPage.html") + toContextClick = driver.find_element_by_id("doubleClickField") + + contextClick = ActionChains(driver) \ + .context_click(toContextClick) + + contextClick.perform() + assert "ContextClicked" == toContextClick.get_attribute('value') + + +def testMoveAndClick(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("javascriptPage.html") + toClick = driver.find_element_by_id("clickField") + + click = ActionChains(driver) \ + .move_to_element(toClick) \ + .click() + + click.perform() + assert "Clicked" == toClick.get_attribute('value') + + +def testCannotMoveToANullLocator(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("javascriptPage.html") + + with pytest.raises(AttributeError): + move = ActionChains(driver) \ + .move_to_element(None) + move.perform() + + +@pytest.mark.xfail_firefox +def testClickingOnFormElements(driver, pages): + """Copied from org.openqa.selenium.interactions.CombinedInputActionsTest.""" + pages.load("formSelectionPage.html") + options = driver.find_elements_by_tag_name("option") + selectThreeOptions = ActionChains(driver) \ + .click(options[1]) \ + .key_down(Keys.SHIFT) \ + .click(options[3]) \ + .key_up(Keys.SHIFT) + selectThreeOptions.perform() + + showButton = driver.find_element_by_name("showselected") + showButton.click() + + resultElement = driver.find_element_by_id("result") + assert "roquefort parmigiano cheddar" == resultElement.text + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1292178') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1292178') +def testSelectingMultipleItems(driver, pages): + """Copied from org.openqa.selenium.interactions.CombinedInputActionsTest.""" + pages.load("selectableItems.html") + reportingElement = driver.find_element_by_id("infodiv") + assert "no info" == reportingElement.text + + listItems = driver.find_elements_by_tag_name("li") + selectThreeItems = ActionChains(driver) \ + .key_down(Keys.CONTROL) \ + .click(listItems[1]) \ + .click(listItems[3]) \ + .click(listItems[5]) \ + .key_up(Keys.CONTROL) + selectThreeItems.perform() + + assert "#item2 #item4 #item6" == reportingElement.text + + # Now click on another element, make sure that's the only one selected. + actionsBuilder = ActionChains(driver) + actionsBuilder.click(listItems[6]).perform() + assert "#item7" == reportingElement.text + + +@pytest.mark.xfail_marionette( + reason='https://github.com/mozilla/geckodriver/issues/646') +@pytest.mark.xfail_remote( + reason='https://github.com/mozilla/geckodriver/issues/646') +def testSendingKeysToActiveElementWithModifier(driver, pages): + pages.load("formPage.html") + e = driver.find_element_by_id("working") + e.click() + + ActionChains(driver) \ + .key_down(Keys.SHIFT) \ + .send_keys("abc") \ + .key_up(Keys.SHIFT) \ + .perform() + + assert "ABC" == e.get_attribute('value') + + +def testSendingKeysToElement(driver, pages): + pages.load("formPage.html") + e = driver.find_element_by_id("working") + + ActionChains(driver).send_keys_to_element(e, 'abc').perform() + + assert "abc" == e.get_attribute('value') + + +def testCanSendKeysBetweenClicks(driver, pages): + """ + For W3C, ensures that the correct number of pauses are given to the other + input device. + """ + pages.load('javascriptPage.html') + keyup = driver.find_element_by_id("keyUp") + keydown = driver.find_element_by_id("keyDown") + ActionChains(driver).click(keyup).send_keys('foobar').click(keydown).perform() + + assert 'foobar' == keyup.get_attribute('value') + + +def test_can_reset_interactions(driver, pages): + actions = ActionChains(driver) + actions.click() + actions.key_down('A') + if driver.w3c: + assert all((len(device.actions) > 0 for device in actions.w3c_actions.devices)) + else: + assert len(actions._actions) > 0 + + actions.reset_actions() + + if driver.w3c: + assert all((len(device.actions) == 0 for device in actions.w3c_actions.devices)) + else: + assert len(actions._actions) == 0 + + +def test_can_pause(driver, pages): + from time import time + pages.load("javascriptPage.html") + + pause_time = 2 + toClick = driver.find_element_by_id("clickField") + toDoubleClick = driver.find_element_by_id("doubleClickField") + + pause = ActionChains(driver).click(toClick).pause(pause_time).click(toDoubleClick) + + start = time() + pause.perform() + end = time() + + assert pause_time < end - start + assert "Clicked" == toClick.get_attribute('value') + assert "Clicked" == toDoubleClick.get_attribute('value') diff --git a/test/selenium/webdriver/common/network.py b/test/selenium/webdriver/common/network.py new file mode 100644 index 00000000..b36deb57 --- /dev/null +++ b/test/selenium/webdriver/common/network.py @@ -0,0 +1,55 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# module for getting the lan ip address of the computer +import os +import socket + +if os.name != "nt": + import fcntl + import struct + + def get_interface_ip(ifname): + def _bytes(value, encoding): + try: + return bytes(value, encoding) # Python 3 + except TypeError: + return value # Python 2 + + sckt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + return socket.inet_ntoa(fcntl.ioctl( + sckt.fileno(), + 0x8915, # SIOCGIFADDR + struct.pack('256s', _bytes(ifname[:15], 'utf-8')) + )[20:24]) + + +def get_lan_ip(): + try: + ip = socket.gethostbyname(socket.gethostname()) + except Exception: + return '0.0.0.0' + if ip.startswith("127.") and os.name != "nt": + interfaces = ["eth0", "eth1", "eth2", "en0", "en1", "en2", "en3", + "en4", "wlan0", "wlan1", "wifi0", "ath0", "ath1", "ppp0"] + for ifname in interfaces: + try: + ip = get_interface_ip(ifname) + break + except IOError: + pass + return ip diff --git a/test/selenium/webdriver/common/opacity_tests.py b/test/selenium/webdriver/common/opacity_tests.py new file mode 100644 index 00000000..1593e8d6 --- /dev/null +++ b/test/selenium/webdriver/common/opacity_tests.py @@ -0,0 +1,47 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By + + +@pytest.mark.xfail_ie +def testShouldBeAbleToClickOnElementsWithOpacityZero(driver, pages): + pages.load("click_jacker.html") + element = driver.find_element(By.ID, "clickJacker") + assert '0' == element.value_of_css_property("opacity"), \ + "Precondition failed: clickJacker should be transparent.\ + Value was %s" % element.value_of_css_property("opacity") + element.click() + assert '1' == element.value_of_css_property("opacity") + + +@pytest.mark.xfail_ie +def testShouldBeAbleToSelectOptionsFromAnInvisibleSelect(driver, pages): + pages.load("formPage.html") + select = driver.find_element(By.ID, "invisi_select") + options = select.find_elements(By.TAG_NAME, "option") + apples = options[0] + oranges = options[1] + + assert apples.is_selected() + assert not oranges.is_selected() + + oranges.click() + assert not apples.is_selected() + assert oranges.is_selected() diff --git a/test/selenium/webdriver/common/page_load_timeout_tests.py b/test/selenium/webdriver/common/page_load_timeout_tests.py new file mode 100644 index 00000000..19c83aaf --- /dev/null +++ b/test/selenium/webdriver/common/page_load_timeout_tests.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import TimeoutException + + +@pytest.fixture(autouse=True) +def reset_timeouts(driver): + yield + driver.set_page_load_timeout(300) + + +def testShouldTimeoutOnPageLoadTakingTooLong(driver, pages): + driver.set_page_load_timeout(0.01) + with pytest.raises(TimeoutException): + pages.load("simpleTest.html") + + +@pytest.mark.xfail_chrome +def testClickShouldTimeout(driver, pages): + pages.load("simpleTest.html") + driver.set_page_load_timeout(0.01) + with pytest.raises(TimeoutException): + driver.find_element_by_id("multilinelink").click() diff --git a/test/selenium/webdriver/common/page_loader.py b/test/selenium/webdriver/common/page_loader.py new file mode 100644 index 00000000..8af8fcac --- /dev/null +++ b/test/selenium/webdriver/common/page_loader.py @@ -0,0 +1,32 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""This module contains some decorators that can be used to support +the page models. For example for an action that needs a page to be fully +loaded, the @require_loaded decorator will make sure the page is loaded +before the call is invoked. +This pattern is also useful for waiting for certain asynchronous events +to happen before excuting certain actions.""" + + +def require_loaded(func): + def load_page(page, *params, **kwds): + if not page.is_loaded(): + page.load() + assert page.is_loaded(), "page should be loaded by now" + return func(page, *params, **kwds) + return load_page diff --git a/test/selenium/webdriver/common/page_loading_tests.py b/test/selenium/webdriver/common/page_loading_tests.py new file mode 100644 index 00000000..2f192804 --- /dev/null +++ b/test/selenium/webdriver/common/page_loading_tests.py @@ -0,0 +1,132 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + + +def testShouldWaitForDocumentToBeLoaded(driver, pages): + pages.load("simpleTest.html") + assert driver.title == "Hello WebDriver" + + +# Disabled till Java WebServer is used +# def testShouldFollowRedirectsSentInTheHttpResponseHeaders(driver, pages): +# pages.load("redirect.html") +# assert driver.title == "We Arrive Here" + + +# Disabled till the Java WebServer is used +# def testShouldFollowMetaRedirects(driver, pages): +# pages.load("metaRedirect.html") +# assert driver.title == "We Arrive Here" + + +def testShouldBeAbleToGetAFragmentOnTheCurrentPage(driver, pages): + pages.load("xhtmlTest.html") + location = driver.current_url + driver.get(location + "#text") + driver.find_element(by=By.ID, value="id1") + + +@pytest.mark.xfail_marionette(raises=WebDriverException) +@pytest.mark.xfail_remote(raises=WebDriverException) +def testShouldReturnWhenGettingAUrlThatDoesNotResolve(driver): + # Of course, we're up the creek if this ever does get registered + driver.get("http://www.thisurldoesnotexist.comx/") + + +@pytest.mark.xfail_marionette(raises=WebDriverException) +@pytest.mark.xfail_remote(raises=WebDriverException) +def testShouldReturnWhenGettingAUrlThatDoesNotConnect(driver): + # Here's hoping that there's nothing here. There shouldn't be + driver.get("http://localhost:3001") + +# def testShouldBeAbleToLoadAPageWithFramesetsAndWaitUntilAllFramesAreLoaded() { +# driver.get(pages.framesetPage) +# +# driver.switchTo().frame(0) +# WebElement pageNumber = driver.findElement(By.xpath("#span[@id='pageNumber']")) +# self.assertEqual((pageNumber.getText().trim(), equalTo("1")) +# +# driver.switchTo().defaultContent().switchTo().frame(1) +# pageNumber = driver.findElement(By.xpath("#span[@id='pageNumber']")) +# self.assertEqual((pageNumber.getText().trim(), equalTo("2")) + +# Need to implement this decorator +# @NeedsFreshDriver +# def testSouldDoNothingIfThereIsNothingToGoBackTo() { +# originalTitle = driver.getTitle(); +# driver.get(pages.formPage); +# driver.back(); +# # We may have returned to the browser's home page +# self.assertEqual(driver.title, anyOf(equalTo(originalTitle), equalTo("We Leave From Here"))); + + +def testShouldBeAbleToNavigateBackInTheBrowserHistory(driver, pages): + pages.load("formPage.html") + + driver.find_element(by=By.ID, value="imageButton").submit() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + driver.back() + assert driver.title == "We Leave From Here" + + +def testShouldBeAbleToNavigateBackInTheBrowserHistoryInPresenceOfIframes(driver, pages): + pages.load("xhtmlTest.html") + + driver.find_element(by=By.NAME, value="sameWindow").click() + + assert driver.title == "This page has iframes" + + driver.back() + assert driver.title == "XHTML Test Page" + + +def testShouldBeAbleToNavigateForwardsInTheBrowserHistory(driver, pages): + pages.load("formPage.html") + + driver.find_element(by=By.ID, value="imageButton").submit() + WebDriverWait(driver, 3).until(EC.title_is("We Arrive Here")) + + driver.back() + assert driver.title == "We Leave From Here" + + driver.forward() + assert driver.title == "We Arrive Here" + + +@pytest.mark.xfail_ie +@pytest.mark.xfail_marionette(run=False) +@pytest.mark.xfail_remote(run=False) +@pytest.mark.xfail_chrome(run=False) +def testShouldNotHangifDocumentOpenCallIsNeverFollowedByDocumentCloseCall(driver, pages): + pages.load("document_write_in_onload.html") + driver.find_element(By.XPATH, "//body") + + +def testShouldBeAbleToRefreshAPage(driver, pages): + pages.load("xhtmlTest.html") + + driver.refresh() + + assert driver.title == "XHTML Test Page" diff --git a/test/selenium/webdriver/common/position_and_size_tests.py b/test/selenium/webdriver/common/position_and_size_tests.py new file mode 100644 index 00000000..07252414 --- /dev/null +++ b/test/selenium/webdriver/common/position_and_size_tests.py @@ -0,0 +1,110 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By + + +def testShouldBeAbleToDetermineTheLocationOfAnElement(driver, pages): + pages.load("xhtmlTest.html") + location = driver.find_element(By.ID, "username").location_once_scrolled_into_view + assert location["x"] > 0 + assert location["y"] > 0 + + +@pytest.mark.parametrize('page', ( + 'coordinates_tests/simple_page.html', + 'coordinates_tests/page_with_empty_element.html', + 'coordinates_tests/page_with_transparent_element.html', + 'coordinates_tests/page_with_hidden_element.html'), + ids=('basic', 'empty', 'transparent', 'hidden')) +def testShouldGetCoordinatesOfAnElement(page, driver, pages): + pages.load(page) + element = driver.find_element(By.ID, "box") + _check_location(element.location_once_scrolled_into_view, x=10, y=10) + _check_location(element.location, x=10, y=10) + + +def testShouldGetCoordinatesOfAnInvisibleElement(driver, pages): + pages.load("coordinates_tests/page_with_invisible_element.html") + element = driver.find_element(By.ID, "box") + _check_location(element.location_once_scrolled_into_view, x=0, y=0) + _check_location(element.location, x=0, y=0) + + +def testShouldScrollPageAndGetCoordinatesOfAnElementThatIsOutOfViewPort(driver, pages): + pages.load("coordinates_tests/page_with_element_out_of_view.html") + element = driver.find_element(By.ID, "box") + windowHeight = driver.get_window_size()["height"] + _check_location(element.location_once_scrolled_into_view, x=10) + assert 0 <= element.location_once_scrolled_into_view["y"] <= (windowHeight - 100) + _check_location(element.location, x=10, y=5010) + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_marionette +@pytest.mark.xfail_remote +def testShouldGetCoordinatesOfAnElementInAFrame(driver, pages): + pages.load("coordinates_tests/element_in_frame.html") + driver.switch_to.frame(driver.find_element(By.NAME, "ifr")) + element = driver.find_element(By.ID, "box") + _check_location(element.location_once_scrolled_into_view, x=25, y=25) + _check_location(element.location, x=10, y=10) + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_marionette +@pytest.mark.xfail_remote +def testShouldGetCoordinatesOfAnElementInANestedFrame(driver, pages): + pages.load("coordinates_tests/element_in_nested_frame.html") + driver.switch_to.frame(driver.find_element(By.NAME, "ifr")) + driver.switch_to.frame(driver.find_element(By.NAME, "ifr")) + element = driver.find_element(By.ID, "box") + _check_location(element.location_once_scrolled_into_view, x=40, y=40) + _check_location(element.location, x=10, y=10) + + +def testShouldGetCoordinatesOfAnElementWithFixedPosition(driver, pages): + pages.load("coordinates_tests/page_with_fixed_element.html") + element = driver.find_element(By.ID, "fixed") + _check_location(element.location_once_scrolled_into_view, y=0) + _check_location(element.location, y=0) + + driver.find_element(By.ID, "bottom").click() + _check_location(element.location_once_scrolled_into_view, y=0) + assert element.location["y"] > 0 + + +def testShouldCorrectlyIdentifyThatAnElementHasWidthAndHeight(driver, pages): + pages.load("xhtmlTest.html") + shrinko = driver.find_element(By.ID, "linkId") + size = shrinko.size + assert size["width"] > 0 + assert size["height"] > 0 + + +def _check_location(location, **kwargs): + try: + # python 2.x + expected = kwargs.viewitems() + actual = location.viewitems() + except AttributeError: + # python 3.x + expected = kwargs.items() + actual = location.items() + assert expected <= actual diff --git a/test/selenium/webdriver/common/proxy_tests.py b/test/selenium/webdriver/common/proxy_tests.py new file mode 100755 index 00000000..b351f833 --- /dev/null +++ b/test/selenium/webdriver/common/proxy_tests.py @@ -0,0 +1,145 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.proxy import Proxy, ProxyType + + +MANUAL_PROXY = { + 'httpProxy': 'some.url:1234', + 'ftpProxy': 'ftp.proxy', + 'noProxy': 'localhost, foo.localhost', + 'sslProxy': 'ssl.proxy:1234', + 'socksProxy': 'socks.proxy:65555', + 'socksUsername': 'test', + 'socksPassword': 'test', + 'socksVersion': 5, +} + +PAC_PROXY = { + 'proxyAutoconfigUrl': 'http://pac.url:1234', +} + +AUTODETECT_PROXY = { + 'autodetect': True, +} + + +def testCanAddManualProxyToDesiredCapabilities(): + proxy = Proxy() + proxy.http_proxy = MANUAL_PROXY['httpProxy'] + proxy.ftp_proxy = MANUAL_PROXY['ftpProxy'] + proxy.no_proxy = MANUAL_PROXY['noProxy'] + proxy.sslProxy = MANUAL_PROXY['sslProxy'] + proxy.socksProxy = MANUAL_PROXY['socksProxy'] + proxy.socksUsername = MANUAL_PROXY['socksUsername'] + proxy.socksPassword = MANUAL_PROXY['socksPassword'] + proxy.socksVersion = MANUAL_PROXY['socksVersion'] + + desired_capabilities = {} + proxy.add_to_capabilities(desired_capabilities) + + proxy_capabilities = MANUAL_PROXY.copy() + proxy_capabilities['proxyType'] = 'MANUAL' + expected_capabilities = {'proxy': proxy_capabilities} + assert expected_capabilities == desired_capabilities + + +def testCanAddAutodetectProxyToDesiredCapabilities(): + proxy = Proxy() + proxy.auto_detect = AUTODETECT_PROXY['autodetect'] + + desired_capabilities = {} + proxy.add_to_capabilities(desired_capabilities) + + proxy_capabilities = AUTODETECT_PROXY.copy() + proxy_capabilities['proxyType'] = 'AUTODETECT' + expected_capabilities = {'proxy': proxy_capabilities} + assert expected_capabilities == desired_capabilities + + +def testCanAddPACProxyToDesiredCapabilities(): + proxy = Proxy() + proxy.proxy_autoconfig_url = PAC_PROXY['proxyAutoconfigUrl'] + + desired_capabilities = {} + proxy.add_to_capabilities(desired_capabilities) + + proxy_capabilities = PAC_PROXY.copy() + proxy_capabilities['proxyType'] = 'PAC' + expected_capabilities = {'proxy': proxy_capabilities} + assert expected_capabilities == desired_capabilities + + +def testCanNotChangeInitializedProxyType(): + proxy = Proxy(raw={'proxyType': 'direct'}) + with pytest.raises(Exception): + proxy.proxy_type = ProxyType.SYSTEM + + proxy = Proxy(raw={'proxyType': ProxyType.DIRECT}) + with pytest.raises(Exception): + proxy.proxy_type = ProxyType.SYSTEM + + +def testCanInitManualProxy(): + proxy = Proxy(raw=MANUAL_PROXY) + + assert ProxyType.MANUAL == proxy.proxy_type + assert MANUAL_PROXY['httpProxy'] == proxy.http_proxy + assert MANUAL_PROXY['ftpProxy'] == proxy.ftp_proxy + assert MANUAL_PROXY['noProxy'] == proxy.no_proxy + assert MANUAL_PROXY['sslProxy'] == proxy.sslProxy + assert MANUAL_PROXY['socksProxy'] == proxy.socksProxy + assert MANUAL_PROXY['socksUsername'] == proxy.socksUsername + assert MANUAL_PROXY['socksPassword'] == proxy.socksPassword + assert MANUAL_PROXY['socksVersion'] == proxy.socksVersion + + +def testCanInitAutodetectProxy(): + proxy = Proxy(raw=AUTODETECT_PROXY) + assert ProxyType.AUTODETECT == proxy.proxy_type + assert AUTODETECT_PROXY['autodetect'] == proxy.auto_detect + + +def testCanInitPACProxy(): + proxy = Proxy(raw=PAC_PROXY) + assert ProxyType.PAC == proxy.proxy_type + assert PAC_PROXY['proxyAutoconfigUrl'] == proxy.proxy_autoconfig_url + + +def testCanInitEmptyProxy(): + proxy = Proxy() + assert ProxyType.UNSPECIFIED == proxy.proxy_type + assert '' == proxy.http_proxy + assert '' == proxy.ftp_proxy + assert '' == proxy.no_proxy + assert '' == proxy.sslProxy + assert '' == proxy.socksProxy + assert '' == proxy.socksUsername + assert '' == proxy.socksPassword + assert proxy.auto_detect is False + assert '' == proxy.proxy_autoconfig_url + assert proxy.socks_version is None + + desired_capabilities = {} + proxy.add_to_capabilities(desired_capabilities) + + proxy_capabilities = {} + proxy_capabilities['proxyType'] = 'UNSPECIFIED' + expected_capabilities = {'proxy': proxy_capabilities} + assert expected_capabilities == desired_capabilities diff --git a/test/selenium/webdriver/common/quit_tests.py b/test/selenium/webdriver/common/quit_tests.py new file mode 100644 index 00000000..2faac425 --- /dev/null +++ b/test/selenium/webdriver/common/quit_tests.py @@ -0,0 +1,25 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + + +@pytest.mark.no_driver_after_test +def test_quit(driver, pages): + driver.quit() + with pytest.raises(Exception): + pages.load('simpleTest.html') diff --git a/test/selenium/webdriver/common/rendered_webelement_tests.py b/test/selenium/webdriver/common/rendered_webelement_tests.py new file mode 100644 index 00000000..9c5aca0a --- /dev/null +++ b/test/selenium/webdriver/common/rendered_webelement_tests.py @@ -0,0 +1,65 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.color import Color + + +def testShouldPickUpStyleOfAnElement(driver, pages): + pages.load("javascriptPage.html") + + element = driver.find_element(by=By.ID, value="green-parent") + backgroundColour = Color.from_string(element.value_of_css_property("background-color")) + assert Color.from_string("rgba(0, 128, 0, 1)") == backgroundColour + + element = driver.find_element(by=By.ID, value="red-item") + backgroundColour = Color.from_string(element.value_of_css_property("background-color")) + assert Color.from_string("rgba(255, 0, 0, 1)") == backgroundColour + + +def testShouldAllowInheritedStylesToBeUsed(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="green-item") + backgroundColour = Color.from_string(element.value_of_css_property("background-color")) + assert backgroundColour == Color.from_string("transparent") + + +def testShouldCorrectlyIdentifyThatAnElementHasWidth(driver, pages): + pages.load("xhtmlTest.html") + + shrinko = driver.find_element(by=By.ID, value="linkId") + size = shrinko.size + assert size["width"] > 0 + assert size["height"] > 0 + + +@pytest.mark.xfail_safari( + reason='Get Element Rect command not implemented', + raises=WebDriverException) +def testShouldBeAbleToDetermineTheRectOfAnElement(driver, pages): + pages.load("xhtmlTest.html") + + element = driver.find_element(By.ID, "username") + rect = element.rect + + assert rect["x"] > 0 + assert rect["y"] > 0 + assert rect["width"] > 0 + assert rect["height"] > 0 diff --git a/test/selenium/webdriver/common/repr_tests.py b/test/selenium/webdriver/common/repr_tests.py new file mode 100644 index 00000000..7c830a86 --- /dev/null +++ b/test/selenium/webdriver/common/repr_tests.py @@ -0,0 +1,40 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.support.wait import WebDriverWait + + +def testShouldImplementReprForWebDriver(driver): + driver_repr = repr(driver) + assert type(driver).__name__ in driver_repr + assert driver.session_id in driver_repr + + +def testShouldImplementReprForWebElement(driver, pages): + pages.load('simpleTest.html') + elem = driver.find_element_by_id("validImgTag") + elem_repr = repr(elem) + assert type(elem).__name__ in elem_repr + assert driver.session_id in elem_repr + assert elem._id in elem_repr + + +def testShouldImplementReprForWait(driver): + wait = WebDriverWait(driver, 30) + wait_repr = repr(wait) + assert type(wait).__name__ in wait_repr + assert driver.session_id in wait_repr diff --git a/test/selenium/webdriver/common/results_page.py b/test/selenium/webdriver/common/results_page.py new file mode 100644 index 00000000..4c4dd0c8 --- /dev/null +++ b/test/selenium/webdriver/common/results_page.py @@ -0,0 +1,37 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +class ResultsPage(object): + """This class models a google search result page.""" + + def __init__(self, driver): + self._driver = driver + + def is_loaded(self): + return "/search" in self._driver.get_current_url() + + def load(self): + raise Exception("This page shouldn't be loaded directly") + + def link_contains_match_for(self, term): + result_section = self._driver.find_element_by_id("res") + elements = result_section.find_elements_by_xpath(".//*[@class='l']") + for e in elements: + if term in e.get_text(): + return True + return False diff --git a/test/selenium/webdriver/common/select_class_tests.py b/test/selenium/webdriver/common/select_class_tests.py new file mode 100644 index 00000000..99fbca3d --- /dev/null +++ b/test/selenium/webdriver/common/select_class_tests.py @@ -0,0 +1,322 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import ( + ElementNotSelectableException, + NoSuchElementException, + UnexpectedTagNameException) +from selenium.webdriver.support.ui import Select +from selenium.webdriver.common.by import By + +disabledSelect = {'name': 'no-select', 'values': ['Foo']} +singleSelectValues1 = {'name': 'selectomatic', 'values': ['One', 'Two', 'Four', 'Still learning how to count, apparently']} +singleSelectValues2 = {'name': 'redirect', 'values': ['One', 'Two']} +singleSelectValuesWithSpaces = {'name': 'select_with_spaces', 'values': ['One', 'Two', 'Four', 'Still learning how to count, apparently']} +multiSelectValues1 = {'name': 'multi', 'values': ['Eggs', 'Ham', 'Sausages', 'Onion gravy']} +multiSelectValues2 = {'name': 'select_empty_multiple', 'values': ['select_1', 'select_2', 'select_3', 'select_4']} + + +def testSelectByIndexSingle(driver, pages): + pages.load("formPage.html") + + for select in [singleSelectValues1]: + sel = Select(driver.find_element(By.NAME, select['name'])) + for x in range(len(select['values'])): + sel.select_by_index(x) + assert sel.first_selected_option.text == select['values'][x] + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote +@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403') +def testSelectDisabledByIndex(driver, pages): + pages.load("formPage.html") + sel = Select(driver.find_element(By.NAME, disabledSelect['name'])) + if driver.w3c: + selected = sel.first_selected_option + sel.select_by_index(1) + assert selected == sel.first_selected_option + else: + with pytest.raises(ElementNotSelectableException): + sel.select_by_index(1) + + +def testSelectByValueSingle(driver, pages): + pages.load("formPage.html") + + for select in [singleSelectValues1]: + sel = Select(driver.find_element(By.NAME, select['name'])) + for x in range(len(select['values'])): + sel.select_by_value(select['values'][x].lower()) + assert sel.first_selected_option.text == select['values'][x] + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote +@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403') +def testSelectDisabledByValue(driver, pages): + pages.load("formPage.html") + sel = Select(driver.find_element(By.NAME, disabledSelect['name'])) + if driver.w3c: + selected = sel.first_selected_option + sel.select_by_value('bar') + assert selected == sel.first_selected_option + else: + with pytest.raises(ElementNotSelectableException): + sel.select_by_value('bar') + + +def testSelectByVisibleTextSingle(driver, pages): + pages.load("formPage.html") + + for select in [singleSelectValues1]: + sel = Select(driver.find_element(By.NAME, select['name'])) + for x in range(len(select['values'])): + print(select['values'][x]) + sel.select_by_visible_text(select['values'][x]) + assert sel.first_selected_option.text == select['values'][x] + + +@pytest.mark.xfail_chrome( + reason='https://bugs.chromium.org/p/chromedriver/issues/detail?id=822') +def testSelectByVisibleTextShouldNormalizeSpaces(driver, pages): + pages.load("formPage.html") + + for select in [singleSelectValuesWithSpaces]: + sel = Select(driver.find_element(By.NAME, select['name'])) + for x in range(len(select['values'])): + print(select['values'][x]) + sel.select_by_visible_text(select['values'][x]) + assert sel.first_selected_option.text == select['values'][x] + + +@pytest.mark.xfail_chrome +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote +@pytest.mark.xfail_marionette(reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1429403') +def testSelectDisabledByVisibleText(driver, pages): + pages.load("formPage.html") + sel = Select(driver.find_element(By.NAME, disabledSelect['name'])) + if driver.w3c: + selected = sel.first_selected_option + sel.select_by_visible_text('Bar') + assert selected == sel.first_selected_option + else: + with pytest.raises(ElementNotSelectableException): + sel.select_by_visible_text('Bar') + + +def testSelectByIndexMultiple(driver, pages): + pages.load("formPage.html") + + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + for x in range(len(select['values'])): + sel.select_by_index(x) + selected = sel.all_selected_options + assert len(selected) == x + 1 + for j in range(len(selected)): + assert selected[j].text == select['values'][j] + + +def testSelectByValueMultiple(driver, pages): + pages.load("formPage.html") + + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + for x in range(len(select['values'])): + sel.select_by_value(select['values'][x].lower()) + selected = sel.all_selected_options + assert len(selected) == x + 1 + for j in range(len(selected)): + assert selected[j].text == select['values'][j] + + +def testSelectByVisibleTextMultiple(driver, pages): + pages.load("formPage.html") + + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + for x in range(len(select['values'])): + sel.select_by_visible_text(select['values'][x]) + selected = sel.all_selected_options + assert len(selected) == x + 1 + for j in range(len(selected)): + assert selected[j].text == select['values'][j] + + +def testDeselectAllSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2]: + with pytest.raises(NotImplementedError): + Select(driver.find_element(By.NAME, select['name'])).deselect_all() + + +def testDeselectAllMultiple(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + assert len(sel.all_selected_options) == 0 + + +def testDeselectByIndexSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2]: + with pytest.raises(NotImplementedError): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_index(0) + + +def testDeselectByValueSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2]: + with pytest.raises(NotImplementedError): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_value(select['values'][0].lower()) + + +def testDeselectByVisibleTextSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2]: + with pytest.raises(NotImplementedError): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_visible_text(select['values'][0]) + + +def testDeselectByIndexMultiple(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + sel.select_by_index(0) + sel.select_by_index(1) + sel.select_by_index(2) + sel.select_by_index(3) + sel.deselect_by_index(1) + sel.deselect_by_index(3) + selected = sel.all_selected_options + assert len(selected) == 2 + assert selected[0].text == select['values'][0] + assert selected[1].text == select['values'][2] + + +def testDeselectByValueMultiple(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + sel.select_by_index(0) + sel.select_by_index(1) + sel.select_by_index(2) + sel.select_by_index(3) + sel.deselect_by_value(select['values'][1].lower()) + sel.deselect_by_value(select['values'][3].lower()) + selected = sel.all_selected_options + assert len(selected) == 2 + assert selected[0].text == select['values'][0] + assert selected[1].text == select['values'][2] + + +def testDeselectByVisibleTextMultiple(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + sel = Select(driver.find_element(By.NAME, select['name'])) + sel.deselect_all() + sel.select_by_index(0) + sel.select_by_index(1) + sel.select_by_index(2) + sel.select_by_index(3) + sel.deselect_by_visible_text(select['values'][1]) + sel.deselect_by_visible_text(select['values'][3]) + selected = sel.all_selected_options + assert len(selected) == 2 + assert selected[0].text == select['values'][0] + assert selected[1].text == select['values'][2] + + +def testGetOptions(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2, multiSelectValues1, multiSelectValues2]: + opts = Select(driver.find_element(By.NAME, select['name'])).options + assert len(opts) == len(select['values']) + for i in range(len(opts)): + assert opts[i].text == select['values'][i] + + +def testGetAllSelectedOptionsSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2, disabledSelect]: + opts = Select(driver.find_element(By.NAME, select['name'])).all_selected_options + assert len(opts) == 1 + assert opts[0].text == select['values'][0] + + +def testGetAllSelectedOptionsMultiple(driver, pages): + pages.load("formPage.html") + opts = Select(driver.find_element(By.NAME, multiSelectValues1['name'])).all_selected_options + assert len(opts) == 2 + assert opts[0].text, multiSelectValues1['values'][0] + assert opts[1].text, multiSelectValues1['values'][2] + opts = Select(driver.find_element(By.NAME, multiSelectValues2['name'])).all_selected_options + assert len(opts) == 0 + + +def testGetFirstSelectedOptionSingle(driver, pages): + pages.load("formPage.html") + for select in [singleSelectValues1, singleSelectValues2]: + opt = Select(driver.find_element(By.NAME, select['name'])).first_selected_option + assert opt.text == select['values'][0] + + +def testGetFirstSelectedOptionMultiple(driver, pages): + pages.load("formPage.html") + opt = Select(driver.find_element(By.NAME, multiSelectValues1['name'])).first_selected_option + assert opt.text == multiSelectValues1['values'][0] + opt = Select(driver.find_element(By.NAME, multiSelectValues2['name'])).all_selected_options + assert len(opt) == 0 + + +def testRaisesExceptionForInvalidTagName(driver, pages): + pages.load("formPage.html") + with pytest.raises(UnexpectedTagNameException): + Select(driver.find_element(By.TAG_NAME, "div")) + + +def testDeselectByIndexNonExistent(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + with pytest.raises(NoSuchElementException): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_index(10) + + +def testDeselectByValueNonExistent(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + with pytest.raises(NoSuchElementException): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_value('not there') + + +def testDeselectByTextNonExistent(driver, pages): + pages.load("formPage.html") + for select in [multiSelectValues1, multiSelectValues2]: + with pytest.raises(NoSuchElementException): + Select(driver.find_element(By.NAME, select['name'])).deselect_by_visible_text('not there') diff --git a/test/selenium/webdriver/common/select_element_handling_tests.py b/test/selenium/webdriver/common/select_element_handling_tests.py new file mode 100644 index 00000000..c6bfc7dc --- /dev/null +++ b/test/selenium/webdriver/common/select_element_handling_tests.py @@ -0,0 +1,97 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.by import By + + +def testShouldBePossibleToDeselectASingleOptionFromASelectWhichAllowsMultipleChoice(driver, pages): + pages.load("formPage.html") + multiSelect = driver.find_element(By.ID, "multi") + options = multiSelect.find_elements(By.TAG_NAME, "option") + + option = options[0] + assert option.is_selected() is True + option.click() + assert option.is_selected() is False + option.click() + assert option.is_selected() is True + + option = options[2] + assert option.is_selected() is True + + +def testShouldBeAbleToChangeTheSelectedOptionInASelec(driver, pages): + pages.load("formPage.html") + selectBox = driver.find_element(By.XPATH, "//select[@name='selectomatic']") + options = selectBox.find_elements(By.TAG_NAME, "option") + one = options[0] + two = options[1] + assert one.is_selected() is True + assert two.is_selected() is False + + two.click() + assert one.is_selected() is False + assert two.is_selected() is True + + +def testShouldBeAbleToSelectMoreThanOneOptionFromASelectWhichAllowsMultipleChoice(driver, pages): + pages.load("formPage.html") + + multiSelect = driver.find_element(By.ID, "multi") + options = multiSelect.find_elements(By.TAG_NAME, "option") + for option in options: + if not option.is_selected(): + option.click() + + for i in range(len(options)): + option = options[i] + assert option.is_selected() is True + + +def testShouldSelectFirstOptionaultIfNoneIsSelecte(driver, pages): + pages.load("formPage.html") + selectBox = driver.find_element(By.XPATH, "//select[@name='select-default']") + options = selectBox.find_elements(By.TAG_NAME, "option") + one = options[0] + two = options[1] + assert one.is_selected() is True + assert two.is_selected() is False + + two.click() + assert one.is_selected() is False + assert two.is_selected() is True + + +def testCanSelectElementsInOptGroup(driver, pages): + pages.load("selectPage.html") + element = driver.find_element(By.ID, "two-in-group") + element.click() + assert element.is_selected() is True + + +def testCanGetValueFromOptionViaAttributeWhenAttributeDoesntExis(driver, pages): + pages.load("formPage.html") + element = driver.find_element(By.CSS_SELECTOR, "select[name='select-default'] option") + assert element.get_attribute("value") == "One" + element = driver.find_element(By.ID, "blankOption") + assert element.get_attribute("value") == "" + + +def testCanGetValueFromOptionViaAttributeWhenAttributeIsEmptyString(driver, pages): + pages.load("formPage.html") + element = driver.find_element(By.ID, "optionEmptyValueSet") + assert element.get_attribute("value") == "" diff --git a/test/selenium/webdriver/common/stale_reference_tests.py b/test/selenium/webdriver/common/stale_reference_tests.py new file mode 100644 index 00000000..bc81b256 --- /dev/null +++ b/test/selenium/webdriver/common/stale_reference_tests.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By +from selenium.common.exceptions import StaleElementReferenceException + + +def testOldPage(driver, pages): + pages.load("simpleTest.html") + elem = driver.find_element(by=By.ID, value="links") + pages.load("xhtmlTest.html") + with pytest.raises(StaleElementReferenceException): + elem.click() + + +def testShouldNotCrashWhenCallingGetSizeOnAnObsoleteElement(driver, pages): + pages.load("simpleTest.html") + elem = driver.find_element(by=By.ID, value="links") + pages.load("xhtmlTest.html") + with pytest.raises(StaleElementReferenceException): + elem.size + + +def testShouldNotCrashWhenQueryingTheAttributeOfAStaleElement(driver, pages): + pages.load("xhtmlTest.html") + heading = driver.find_element(by=By.XPATH, value="//h1") + pages.load("simpleTest.html") + with pytest.raises(StaleElementReferenceException): + heading.get_attribute("class") diff --git a/test/selenium/webdriver/common/takes_screenshots_tests.py b/test/selenium/webdriver/common/takes_screenshots_tests.py new file mode 100644 index 00000000..0c049cec --- /dev/null +++ b/test/selenium/webdriver/common/takes_screenshots_tests.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import imghdr + +import pytest + + +def test_get_screenshot_as_base64(driver, pages): + pages.load("simpleTest.html") + result = base64.b64decode(driver.get_screenshot_as_base64()) + assert imghdr.what('', result) == 'png' + + +def test_get_screenshot_as_png(driver, pages): + pages.load("simpleTest.html") + result = driver.get_screenshot_as_png() + assert imghdr.what('', result) == 'png' + + +@pytest.mark.xfail_firefox +def test_get_element_screenshot(driver, pages): + pages.load("simpleTest.html") + element = driver.find_element_by_id("multiline") + result = base64.b64decode(element.screenshot_as_base64) + assert imghdr.what('', result) == 'png' diff --git a/test/selenium/webdriver/common/text_handling_tests.py b/test/selenium/webdriver/common/text_handling_tests.py new file mode 100644 index 00000000..9c970cef --- /dev/null +++ b/test/selenium/webdriver/common/text_handling_tests.py @@ -0,0 +1,203 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.by import By + + +newLine = "\n" + + +def testShouldReturnTheTextContentOfASingleElementWithNoChildren(driver, pages): + pages.load("simpleTest.html") + selectText = driver.find_element(by=By.ID, value="oneline").text + assert selectText == "A single line of text" + + getText = driver.find_element(by=By.ID, value="oneline").text + assert getText == "A single line of text" + + +def testShouldReturnTheEntireTextContentOfChildElements(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="multiline").text + + assert "A div containing" in text + assert "More than one line of text" in text + assert "and block level elements" in text + + +def testShouldIgnoreScriptElements(driver, pages): + pages.load("javascriptEnhancedForm.html") + labelForUsername = driver.find_element(by=By.ID, value="labelforusername") + text = labelForUsername.text + + assert len(labelForUsername.find_elements(by=By.TAG_NAME, value="script")) == 1 + assert "document.getElementById" not in text + assert text == "Username:" + + +def testShouldRepresentABlockLevelElementAsANewline(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="multiline").text + + assert text.startswith("A div containing" + newLine) + assert "More than one line of text" + newLine in text + assert text.endswith("and block level elements") + + +def testShouldCollapseMultipleWhitespaceCharactersIntoASingleSpace(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="lotsofspaces").text + + assert text == "This line has lots of spaces." + + +def testShouldTrimText(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="multiline").text + + assert text.startswith("A div containing") + assert text.endswith("block level elements") + + +def testShouldConvertANonBreakingSpaceIntoANormalSpaceCharacter(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="nbsp").text + + assert text == "This line has a non-breaking space" + + +def testShouldTreatANonBreakingSpaceAsAnyOtherWhitespaceCharacterWhenCollapsingWhitespace(driver, pages): + pages.load("simpleTest.html") + element = driver.find_element(by=By.ID, value="nbspandspaces") + text = element.text + + assert text == "This line has a non-breaking space and spaces" + + +def testHavingInlineElementsShouldNotAffectHowTextIsReturned(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="inline").text + + assert text == "This line has text within elements that are meant to be displayed inline" + + +def testShouldReturnTheEntireTextOfInlineElements(driver, pages): + pages.load("simpleTest.html") + text = driver.find_element(by=By.ID, value="span").text + + assert text == "An inline element" + + +def testShouldBeAbleToSetMoreThanOneLineOfTextInATextArea(driver, pages): + pages.load("formPage.html") + textarea = driver.find_element(by=By.ID, value="withText") + textarea.clear() + + expectedText = "I like cheese" + newLine + newLine + "It's really nice" + + textarea.send_keys(expectedText) + + seenText = textarea.get_attribute("value") + assert seenText == expectedText + + +def testShouldBeAbleToEnterDatesAfterFillingInOtherValuesFirst(driver, pages): + pages.load("formPage.html") + input_ = driver.find_element(by=By.ID, value="working") + expectedValue = "10/03/2007 to 30/07/1993" + input_.send_keys(expectedValue) + seenValue = input_.get_attribute("value") + + assert seenValue == expectedValue + + +def testShouldReturnEmptyStringWhenTextIsOnlySpaces(driver, pages): + pages.load("xhtmlTest.html") + + text = driver.find_element(by=By.ID, value="spaces").text + assert text == "" + + +def testShouldReturnEmptyStringWhenTextIsEmpty(driver, pages): + pages.load("xhtmlTest.html") + + text = driver.find_element(by=By.ID, value="empty").text + assert text == "" + + +@pytest.mark.xfail +def testShouldReturnEmptyStringWhenTagIsSelfClosing(driver, pages): + pages.load("xhtmlFormPage.xhtml") + + text = driver.find_element(by=By.ID, value="self-closed").text + assert text == "" + + +def testShouldHandleSiblingBlockLevelElements(driver, pages): + pages.load("simpleTest.html") + + text = driver.find_element(by=By.ID, value="twoblocks").text + assert text == "Some text" + newLine + "Some more text" + + +def testShouldHandleWhitespaceInInlineElements(driver, pages): + pages.load("simpleTest.html") + + text = driver.find_element(by=By.ID, value="inlinespan").text + assert text == "line has text" + + +def testReadALargeAmountOfData(driver, pages): + pages.load("macbeth.html") + source = driver.page_source.strip().lower() + + assert source.endswith("</html>") + + +def testShouldOnlyIncludeVisibleText(driver, pages): + pages.load("javascriptPage.html") + + empty = driver.find_element(by=By.ID, value="suppressedParagraph").text + explicit = driver.find_element(by=By.ID, value="outer").text + + assert "" == empty + assert "sub-element that is explicitly visible" == explicit + + +def testShouldGetTextFromTableCells(driver, pages): + pages.load("tables.html") + + tr = driver.find_element(by=By.ID, value="hidden_text") + text = tr.text + + assert "some text" in text + assert "some more text" not in text + + +def testShouldGetTextWhichIsAValidJSONObject(driver, pages): + pages.load("simpleTest.html") + element = driver.find_element(by=By.ID, value="simpleJsonText") + assert "{a=\"b\", c=1, d=true}" == element.text + # assert "{a=\"b\", \"c\"=d, e=true, f=\\123\\\\g\\\\\"\"\"\\\'}", element.text) + + +def testShouldGetTextWhichIsAValidComplexJSONObject(driver, pages): + pages.load("simpleTest.html") + element = driver.find_element(by=By.ID, value="complexJsonText") + assert """{a=\"\\\\b\\\\\\\"\'\\\'\"}""" == element.text diff --git a/test/selenium/webdriver/common/typing_tests.py b/test/selenium/webdriver/common/typing_tests.py new file mode 100644 index 00000000..71230f37 --- /dev/null +++ b/test/selenium/webdriver/common/typing_tests.py @@ -0,0 +1,335 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys + +import pytest + + +def testShouldFireKeyPressEvents(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("a") + result = driver.find_element(by=By.ID, value="result") + assert "press:" in result.text + + +def testShouldFireKeyDownEvents(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("I") + result = driver.find_element(by=By.ID, value="result") + assert "down" in result.text + + +def testShouldFireKeyUpEvents(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("a") + result = driver.find_element(by=By.ID, value="result") + assert "up:" in result.text + + +def testShouldTypeLowerCaseLetters(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("abc def") + assert keyReporter.get_attribute("value") == "abc def" + + +def testShouldBeAbleToTypeCapitalLetters(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("ABC DEF") + assert keyReporter.get_attribute("value") == "ABC DEF" + + +def testShouldBeAbleToTypeQuoteMarks(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("\"") + assert keyReporter.get_attribute("value") == "\"" + + +def testShouldBeAbleToTypeTheAtCharacter(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("@") + assert keyReporter.get_attribute("value") == "@" + + +def testShouldBeAbleToMixUpperAndLowerCaseLetters(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("me@eXample.com") + assert keyReporter.get_attribute("value") == "me@eXample.com" + + +def testArrowKeysShouldNotBePrintable(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys(Keys.ARROW_LEFT) + assert keyReporter.get_attribute("value") == "" + + +def testListOfArrowKeysShouldNotBePrintable(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys([Keys.ARROW_LEFT]) + assert keyReporter.get_attribute("value") == "" + + +def testShouldBeAbleToUseArrowKeys(driver, pages): + pages.load("javascriptPage.html") + keyReporter = driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("Tet", Keys.ARROW_LEFT, "s") + assert keyReporter.get_attribute("value") == "Test" + + +def testWillSimulateAKeyUpWhenEnteringTextIntoInputElements(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyUp") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + assert result.text == "I like cheese" + + +def testWillSimulateAKeyDownWhenEnteringTextIntoInputElements(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyDown") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + assert result.text == "I like chees" + + +def testWillSimulateAKeyPressWhenEnteringTextIntoInputElements(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyPress") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + assert result.text == "I like chees" + + +def testWillSimulateAKeyUpWhenEnteringTextIntoTextAreas(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyUpArea") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + assert result.text == "I like cheese" + + +def testWillSimulateAKeyDownWhenEnteringTextIntoTextAreas(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyDownArea") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + assert result.text == "I like chees" + + +def testWillSimulateAKeyPressWhenEnteringTextIntoTextAreas(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyPressArea") + element.send_keys("I like cheese") + result = driver.find_element(by=By.ID, value="result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + assert result.text == "I like chees" + + +def testShouldReportKeyCodeOfArrowKeysUpDownEvents(driver, pages): + pages.load("javascriptPage.html") + result = driver.find_element(by=By.ID, value="result") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys(Keys.ARROW_DOWN) + assert "down: 40" in result.text.strip() + assert "up: 40" in result.text.strip() + + element.send_keys(Keys.ARROW_UP) + assert "down: 38" in result.text.strip() + assert "up: 38" in result.text.strip() + + element.send_keys(Keys.ARROW_LEFT) + assert "down: 37" in result.text.strip() + assert "up: 37" in result.text.strip() + + element.send_keys(Keys.ARROW_RIGHT) + assert "down: 39" in result.text.strip() + assert "up: 39" in result.text.strip() + + # And leave no rubbish/printable keys in the "keyReporter" + assert element.get_attribute("value") == "" + + +def testNumericNonShiftKeys(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + numericLineCharsNonShifted = "`1234567890-=[]\\,.'/42" + element.send_keys(numericLineCharsNonShifted) + assert element.get_attribute("value") == numericLineCharsNonShifted + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +def testNumericShiftKeys(driver, pages): + pages.load("javascriptPage.html") + result = driver.find_element(by=By.ID, value="result") + element = driver.find_element(by=By.ID, value="keyReporter") + numericShiftsEtc = "~!@#$%^&*()_+{}:i\"<>?|END~" + element.send_keys(numericShiftsEtc) + assert element.get_attribute("value") == numericShiftsEtc + assert "up: 16" in result.text.strip() + + +def testLowerCaseAlphaKeys(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + lowerAlphas = "abcdefghijklmnopqrstuvwxyz" + element.send_keys(lowerAlphas) + assert element.get_attribute("value") == lowerAlphas + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +def testUppercaseAlphaKeys(driver, pages): + pages.load("javascriptPage.html") + result = driver.find_element(by=By.ID, value="result") + element = driver.find_element(by=By.ID, value="keyReporter") + upperAlphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + element.send_keys(upperAlphas) + assert element.get_attribute("value") == upperAlphas + assert "up: 16" in result.text.strip() + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +def testAllPrintableKeys(driver, pages): + pages.load("javascriptPage.html") + result = driver.find_element(by=By.ID, value="result") + element = driver.find_element(by=By.ID, value="keyReporter") + allPrintable = "!\"#$%&'()*+,-./0123456789:<=>?@ ABCDEFGHIJKLMNOPQRSTUVWXYZ [\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + element.send_keys(allPrintable) + + assert element.get_attribute("value") == allPrintable + assert "up: 16" in result.text.strip() + + +def testArrowKeysAndPageUpAndDown(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys( + "a" + Keys.LEFT + "b" + Keys.RIGHT + + Keys.UP + Keys.DOWN + Keys.PAGE_UP + Keys.PAGE_DOWN + "1") + assert element.get_attribute("value") == "ba1" + + +# def testHomeAndEndAndPageUpAndPageDownKeys(driver, pages): +# // FIXME: macs don't have HOME keys, would PGUP work? +# if (Platform.getCurrent().is(Platform.MAC)) { +# return +# } + +# pages.load("javascriptPage.html") + +# element = driver.find_element(by=By.ID, value="keyReporter") + +# element.send_keys("abc" + Keys.HOME + "0" + Keys.LEFT + Keys.RIGHT + +# Keys.PAGE_UP + Keys.PAGE_DOWN + Keys.END + "1" + Keys.HOME + +# "0" + Keys.PAGE_UP + Keys.END + "111" + Keys.HOME + "00") +# assert element.get_attribute("value") == "0000abc1111" + + +def testDeleteAndBackspaceKeys(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys("abcdefghi") + assert element.get_attribute("value") == "abcdefghi" + + element.send_keys(Keys.LEFT, Keys.LEFT, Keys.DELETE) + assert element.get_attribute("value") == "abcdefgi" + + element.send_keys(Keys.LEFT, Keys.LEFT, Keys.BACK_SPACE) + assert element.get_attribute("value") == "abcdfgi" + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +def testSpecialSpaceKeys(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys("abcd" + Keys.SPACE + "fgh" + Keys.SPACE + "ij") + assert element.get_attribute("value") == "abcd fgh ij" + + +@pytest.mark.xfail_marionette( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +@pytest.mark.xfail_remote( + reason='https://bugzilla.mozilla.org/show_bug.cgi?id=1255258') +def testNumberpadAndFunctionKeys(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys( + "abcd" + Keys.MULTIPLY + Keys.SUBTRACT + Keys.ADD + + Keys.DECIMAL + Keys.SEPARATOR + Keys.NUMPAD0 + Keys.NUMPAD9 + + Keys.ADD + Keys.SEMICOLON + Keys.EQUALS + Keys.DIVIDE + + Keys.NUMPAD3 + "abcd") + assert element.get_attribute("value") == "abcd*-+.,09+;=/3abcd" + + element.clear() + element.send_keys("FUNCTION" + Keys.F2 + "-KEYS" + Keys.F2) + element.send_keys("" + Keys.F2 + "-TOO" + Keys.F2) + assert element.get_attribute("value") == "FUNCTION-KEYS-TOO" + + +def testShiftSelectionDeletes(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + + element.send_keys("abcd efgh") + assert element.get_attribute("value") == "abcd efgh" + + element.send_keys(Keys.SHIFT, Keys.LEFT, Keys.LEFT, Keys.LEFT) + element.send_keys(Keys.DELETE) + assert element.get_attribute("value") == "abcd e" + + +def testShouldTypeIntoInputElementsThatHaveNoTypeAttribute(driver, pages): + pages.load("formPage.html") + element = driver.find_element(by=By.ID, value="no-type") + element.send_keys("Should Say Cheese") + assert element.get_attribute("value") == "Should Say Cheese" + + +def testShouldTypeAnInteger(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="keyReporter") + element.send_keys(1234) + assert element.get_attribute("value") == "1234" diff --git a/test/selenium/webdriver/common/utils.py b/test/selenium/webdriver/common/utils.py new file mode 100644 index 00000000..863b4256 --- /dev/null +++ b/test/selenium/webdriver/common/utils.py @@ -0,0 +1,26 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def convert_cookie_to_json(cookie): + cookie_dict = {} + for key, value in cookie.items(): + if key == "expires": + cookie_dict["expiry"] = int(value) * 1000 + else: + cookie_dict[key] = value + return cookie_dict diff --git a/test/selenium/webdriver/common/visibility_tests.py b/test/selenium/webdriver/common/visibility_tests.py new file mode 100644 index 00000000..786ba858 --- /dev/null +++ b/test/selenium/webdriver/common/visibility_tests.py @@ -0,0 +1,143 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.common.exceptions import ( + ElementNotVisibleException, + ElementNotInteractableException) +from selenium.webdriver.common.by import By + + +def testShouldAllowTheUserToTellIfAnElementIsDisplayedOrNot(driver, pages): + pages.load("javascriptPage.html") + + assert driver.find_element(by=By.ID, value="displayed").is_displayed() is True + assert driver.find_element(by=By.ID, value="none").is_displayed() is False + assert driver.find_element(by=By.ID, value="suppressedParagraph").is_displayed() is False + assert driver.find_element(by=By.ID, value="hidden").is_displayed() is False + + +def testVisibilityShouldTakeIntoAccountParentVisibility(driver, pages): + pages.load("javascriptPage.html") + + childDiv = driver.find_element(by=By.ID, value="hiddenchild") + hiddenLink = driver.find_element(by=By.ID, value="hiddenlink") + + assert childDiv.is_displayed() is False + assert hiddenLink.is_displayed() is False + + +def testShouldCountElementsAsVisibleIfStylePropertyHasBeenSet(driver, pages): + pages.load("javascriptPage.html") + shown = driver.find_element(by=By.ID, value="visibleSubElement") + assert shown.is_displayed() is True + + +def testShouldModifyTheVisibilityOfAnElementDynamically(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="hideMe") + assert element.is_displayed() is True + element.click() + assert element.is_displayed() is False + + +def testHiddenInputElementsAreNeverVisible(driver, pages): + pages.load("javascriptPage.html") + + shown = driver.find_element(by=By.NAME, value="hidden") + + assert shown.is_displayed() is False + + +def testShouldNotBeAbleToClickOnAnElementThatIsNotDisplayed(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="unclickable") + try: + element.click() + assert 1 == 0, "should have thrown an exception" + except (ElementNotVisibleException, ElementNotInteractableException): + pass + + +def testShouldNotBeAbleToToggleAnElementThatIsNotDisplayed(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="untogglable") + try: + element.click() + assert 1 == 0, "should have thrown an exception" + except (ElementNotVisibleException, ElementNotInteractableException): + pass + + +def testShouldNotBeAbleToSelectAnElementThatIsNotDisplayed(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="untogglable") + try: + element.click() + assert 1 == 0, "should have thrown an exception" + except (ElementNotVisibleException, ElementNotInteractableException): + pass + + +def testShouldNotBeAbleToTypeAnElementThatIsNotDisplayed(driver, pages): + pages.load("javascriptPage.html") + element = driver.find_element(by=By.ID, value="unclickable") + try: + element.send_keys("You don't see me") + assert 1 == 0, "should have thrown an exception" + except (ElementNotVisibleException, ElementNotInteractableException): + pass + assert element.get_attribute("value") != "You don't see me" + + +def testShouldSayElementsWithNegativeTransformAreNotDisplayed(driver, pages): + pages.load('cssTransform.html') + elementX = driver.find_element(By.ID, value='parentX') + assert elementX.is_displayed() is False + elementY = driver.find_element(By.ID, value='parentY') + assert elementY.is_displayed() is False + + +def testShouldSayElementsWithParentWithNegativeTransformAreNotDisplayed(driver, pages): + pages.load('cssTransform.html') + elementX = driver.find_element(By.ID, value='childX') + assert elementX.is_displayed() is False + elementY = driver.find_element(By.ID, value='childY') + assert elementY.is_displayed() is False + + +def testShouldSayElementWithZeroTransformIsVisible(driver, pages): + pages.load('cssTransform.html') + zero_tranform = driver.find_element(By.ID, 'zero-tranform') + assert zero_tranform.is_displayed() is True + + +def testShouldSayElementIsVisibleWhenItHasNegativeTransformButElementisntInANegativeSpace(driver, pages): + pages.load('cssTransform2.html') + zero_tranform = driver.find_element(By.ID, 'negative-percentage-transformY') + assert zero_tranform.is_displayed() is True + + +def testShouldShowElementNotVisibleWithHiddenAttribute(driver, pages): + pages.load('hidden.html') + singleHidden = driver.find_element(By.ID, 'singleHidden') + assert singleHidden.is_displayed() is False + + +def testShouldShowElementNotVisibleWhenParentElementHasHiddenAttribute(driver, pages): + pages.load('hidden.html') + child = driver.find_element(By.ID, 'child') + assert child.is_displayed() is False diff --git a/test/selenium/webdriver/common/w3c_interaction_tests.py b/test/selenium/webdriver/common/w3c_interaction_tests.py new file mode 100644 index 00000000..0673caa6 --- /dev/null +++ b/test/selenium/webdriver/common/w3c_interaction_tests.py @@ -0,0 +1,209 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.actions.action_builder import ActionBuilder +from selenium.webdriver.support.ui import WebDriverWait + + +def test_should_be_able_to_get_pointer_and_keyboard_inputs(driver, pages): + actions = ActionBuilder(driver) + pointers = actions.pointer_inputs + keyboards = actions.key_inputs + + assert pointers is not None + assert keyboards is not None + + +@pytest.mark.xfail_firefox +@pytest.mark.xfail_remote( + reason='https://github.com/mozilla/geckodriver/issues/646') +@pytest.mark.xfail_marionette( + reason='https://github.com/mozilla/geckodriver/issues/646') +def testSendingKeysToActiveElementWithModifier(driver, pages): + pages.load("formPage.html") + e = driver.find_element_by_id("working") + e.click() + + actions = ActionBuilder(driver) + key_action = actions.key_action + key_action.key_down(Keys.SHIFT) \ + .send_keys("abc") \ + .key_up(Keys.SHIFT) + + actions.perform() + + assert "ABC" == e.get_attribute('value') + + +@pytest.mark.xfail_firefox +def test_can_create_pause_action_on_keyboard(driver, pages): + # If we don't get an error and takes less than 3 seconds to run, we are good + import datetime + start = datetime.datetime.now() + actions1 = ActionBuilder(driver) + key_actions = actions1.key_action + key_actions.pause(1) + actions1.perform() + finish = datetime.datetime.now() + assert (finish - start).seconds <= 3 + + # Add a filler step + actions2 = ActionBuilder(driver) + key_action = actions2.key_action + key_action.pause() + actions2.perform() + + +@pytest.mark.xfail_firefox +def test_can_create_pause_action_on_pointer(driver, pages): + # If we don't get an error and takes less than 3 seconds to run, we are good + import datetime + start = datetime.datetime.now() + actions1 = ActionBuilder(driver) + key_actions = actions1.pointer_action + key_actions.pause(1) + actions1.perform() + finish = datetime.datetime.now() + assert (finish - start).seconds <= 3 + + # Add a filler step + actions2 = ActionBuilder(driver) + key_action = actions2.pointer_action + key_action.pause() + actions2.perform() + + +@pytest.mark.xfail_firefox +def test_can_clear_actions(driver, pages): + actions = ActionBuilder(driver) + actions.clear_actions() + + +@pytest.mark.xfail_firefox +def test_move_and_click(driver, pages): + pages.load("javascriptPage.html") + toClick = driver.find_element_by_id("clickField") + + actions = ActionBuilder(driver) + pointer = actions.pointer_action + + pointer.move_to(toClick) \ + .click() + + actions.perform() + assert "Clicked" == toClick.get_attribute('value') + + +@pytest.mark.xfail_firefox +def testDragAndDrop(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + element_available_timeout = 15 + wait = WebDriverWait(driver, element_available_timeout) + pages.load("droppableItems.html") + wait.until(lambda dr: _isElementAvailable(driver, "draggable")) + + if not _isElementAvailable(driver, "draggable"): + raise AssertionError("Could not find draggable element after 15 seconds.") + + toDrag = driver.find_element_by_id("draggable") + dropInto = driver.find_element_by_id("droppable") + actions = ActionBuilder(driver) + pointer = actions.pointer_action + pointer.click_and_hold(toDrag) \ + .move_to(dropInto)\ + .release() + + actions.perform() + + dropInto = driver.find_element_by_id("droppable") + text = dropInto.find_element_by_tag_name("p").text + assert "Dropped!" == text + + +@pytest.mark.xfail_firefox +def test_context_click(driver, pages): + + pages.load("javascriptPage.html") + toContextClick = driver.find_element_by_id("doubleClickField") + + actions = ActionBuilder(driver) + pointer = actions.pointer_action + pointer.context_click(toContextClick) + + actions.perform() + assert "ContextClicked" == toContextClick.get_attribute('value') + + +@pytest.mark.xfail_firefox +def test_double_click(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("javascriptPage.html") + toDoubleClick = driver.find_element_by_id("doubleClickField") + + actions = ActionBuilder(driver) + pointer = actions.pointer_action + + pointer.double_click(toDoubleClick) + + actions.perform() + assert "DoubleClicked" == toDoubleClick.get_attribute('value') + + +@pytest.mark.xfail_firefox +def test_dragging_element_with_mouse_moves_it_to_another_list(driver, pages): + _performDragAndDropWithMouse(driver, pages) + dragInto = driver.find_element_by_id("sortable1") + assert 6 == len(dragInto.find_elements_by_tag_name("li")) + + +@pytest.mark.xfail_firefox +def test_dragging_element_with_mouse_fires_events(driver, pages): + _performDragAndDropWithMouse(driver, pages) + dragReporter = driver.find_element_by_id("dragging_reports") + assert "Nothing happened. DragOut DropIn RightItem 3" == dragReporter.text + + +def _performDragAndDropWithMouse(driver, pages): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + pages.load("draggableLists.html") + dragReporter = driver.find_element_by_id("dragging_reports") + toDrag = driver.find_element_by_id("rightitem-3") + dragInto = driver.find_element_by_id("sortable1") + + actions = ActionBuilder(driver) + pointer = actions.pointer_action + pointer.click_and_hold(toDrag) \ + .move_to(driver.find_element_by_id("leftitem-4")) \ + .move_to(dragInto) \ + .release() + + assert "Nothing happened." == dragReporter.text + + actions.perform() + assert "Nothing happened. DragOut" in dragReporter.text + + +def _isElementAvailable(driver, id): + """Copied from org.openqa.selenium.interactions.TestBasicMouseInterface.""" + try: + driver.find_element_by_id(id) + return True + except Exception: + return False diff --git a/test/selenium/webdriver/common/webdriverwait_tests.py b/test/selenium/webdriver/common/webdriverwait_tests.py new file mode 100644 index 00000000..aab83370 --- /dev/null +++ b/test/selenium/webdriver/common/webdriverwait_tests.py @@ -0,0 +1,323 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import time + +import pytest + +from selenium.common.exceptions import TimeoutException +from selenium.common.exceptions import StaleElementReferenceException +from selenium.common.exceptions import WebDriverException +from selenium.common.exceptions import InvalidElementStateException +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +def throwSERE(driver): + raise StaleElementReferenceException("test") + + +def testShouldExplicitlyWaitForASingleElement(driver, pages): + pages.load("dynamic.html") + add = driver.find_element_by_id("adder") + add.click() + WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.ID, "box0"))) # All is well if this doesn't throw. + + +def testShouldStillFailToFindAnElementWithExplicitWait(driver, pages): + pages.load("dynamic.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.presence_of_element_located((By.ID, "box0"))) + + +def testShouldExplicitlyWaituntilAtLeastOneElementIsFoundWhenSearchingForMany(driver, pages): + pages.load("dynamic.html") + add = driver.find_element_by_id("adder") + + add.click() + add.click() + + elements = WebDriverWait(driver, 2).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "redbox"))) + assert len(elements) >= 1 + + +def testShouldFailToFindElementsWhenExplicitWaiting(driver, pages): + pages.load("dynamic.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.presence_of_all_elements_located((By.CLASS_NAME, "redbox"))) + + +def testShouldWaitUntilAtLeastOneVisibleElementsIsFoundWhenSearchingForMany(driver, pages): + pages.load("hidden_partially.html") + add_visible = driver.find_element_by_id("addVisible") + add_hidden = driver.find_element_by_id("addHidden") + + add_visible.click() + add_visible.click() + add_hidden.click() + + class wait_for_two_elements(object): + + def __init__(self, locator): + self.locator = locator + + def __call__(self, driver): + elements = [element for element in EC._find_elements(driver, self.locator) if EC._element_if_visible(element)] + return elements if len(elements) == 2 else False + + elements = WebDriverWait(driver, 2).until(wait_for_two_elements((By.CLASS_NAME, "redbox"))) + assert len(elements) == 2 + + +def testShouldFailToFindVisibleElementsWhenExplicitWaiting(driver, pages): + pages.load("hidden_partially.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.visibility_of_any_elements_located((By.CLASS_NAME, "redbox"))) + + +def testShouldWaitUntilAllVisibleElementsAreFoundWhenSearchingForMany(driver, pages): + pages.load("hidden_partially.html") + add_visible = driver.find_element_by_id("addVisible") + + add_visible.click() + add_visible.click() + + elements = WebDriverWait(driver, 2).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, "redbox"))) + assert len(elements) == 2 + + +def testShouldFailIfNotAllElementsAreVisible(driver, pages): + pages.load("hidden_partially.html") + add_visible = driver.find_element_by_id("addVisible") + add_hidden = driver.find_element_by_id("addHidden") + + add_visible.click() + add_hidden.click() + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.visibility_of_all_elements_located((By.CLASS_NAME, "redbox"))) + + +def testShouldWaitOnlyAsLongAsTimeoutSpecifiedWhenImplicitWaitsAreSet(driver, pages): + pages.load("dynamic.html") + driver.implicitly_wait(0.5) + start = time.time() + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1).until(EC.presence_of_element_located((By.ID, "box0"))) + assert time.time() - start < 1.5 + + +def testShouldWaitAtLeastOnce(driver, pages): + pages.load("simpleTest.html") + elements = WebDriverWait(driver, 0).until(lambda d: d.find_elements_by_tag_name('h1')) + assert len(elements) >= 1 + + +def testWaitUntilNotReturnsIfEvaluatesToFalse(driver, pages): + assert WebDriverWait(driver, 1).until_not(lambda d: False) is False + + +def testWaitShouldStillFailIfProduceIgnoredException(driver, pages): + ignored = (InvalidElementStateException, StaleElementReferenceException) + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until(throwSERE) + + +def testWaitShouldStillFailIfProduceChildOfIgnoredException(driver, pages): + ignored = (WebDriverException) + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until(throwSERE) + + +def testWaitUntilNotShouldNotFailIfProduceIgnoredException(driver, pages): + ignored = (InvalidElementStateException, StaleElementReferenceException) + assert WebDriverWait(driver, 1, 0.7, ignored_exceptions=ignored).until_not(throwSERE) + + +def testExpectedConditionTitleIs(driver, pages): + pages.load("blank.html") + WebDriverWait(driver, 1).until(EC.title_is("blank")) + driver.execute_script("setTimeout(function(){document.title='not blank'}, 200)") + WebDriverWait(driver, 1).until(EC.title_is("not blank")) + assert driver.title == 'not blank' + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.title_is("blank")) + + +def testExpectedConditionTitleContains(driver, pages): + pages.load("blank.html") + driver.execute_script("setTimeout(function(){document.title='not blank'}, 200)") + WebDriverWait(driver, 1).until(EC.title_contains("not")) + assert driver.title == 'not blank' + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.title_contains("blanket")) + + +def testExpectedConditionVisibilityOfElementLocated(driver, pages): + pages.load("javascriptPage.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.visibility_of_element_located((By.ID, 'clickToHide'))) + driver.find_element_by_id('clickToShow').click() + element = WebDriverWait(driver, 5).until(EC.visibility_of_element_located((By.ID, 'clickToHide'))) + assert element.is_displayed() is True + + +def testExpectedConditionVisibilityOf(driver, pages): + pages.load("javascriptPage.html") + hidden = driver.find_element_by_id('clickToHide') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.visibility_of(hidden)) + driver.find_element_by_id('clickToShow').click() + element = WebDriverWait(driver, 5).until(EC.visibility_of(hidden)) + assert element.is_displayed() is True + + +def testExpectedConditionTextToBePresentInElement(driver, pages): + pages.load('booleanAttributes.html') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.text_to_be_present_in_element((By.ID, 'unwrappable'), 'Expected')) + driver.execute_script("setTimeout(function(){var el = document.getElementById('unwrappable'); el.textContent = el.innerText = 'Unwrappable Expected text'}, 200)") + WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element((By.ID, 'unwrappable'), 'Expected')) + assert 'Unwrappable Expected text' == driver.find_element_by_id('unwrappable').text + + +def testExpectedConditionTextToBePresentInElementValue(driver, pages): + pages.load('booleanAttributes.html') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element_value((By.ID, 'inputRequired'), 'Expected')) + driver.execute_script("setTimeout(function(){document.getElementById('inputRequired').value = 'Example Expected text'}, 200)") + WebDriverWait(driver, 1).until(EC.text_to_be_present_in_element_value((By.ID, 'inputRequired'), 'Expected')) + assert 'Example Expected text' == driver.find_element_by_id('inputRequired').get_attribute('value') + + +def testExpectedConditionFrameToBeAvailableAndSwitchToItByName(driver, pages): + pages.load("blank.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it('myFrame')) + driver.execute_script("setTimeout(function(){var f = document.createElement('iframe'); f.id='myFrame'; f.src = '" + pages.url('iframeWithAlert.html') + "'; document.body.appendChild(f)}, 200)") + WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it('myFrame')) + assert 'click me' == driver.find_element_by_id('alertInFrame').text + + +def testExpectedConditionFrameToBeAvailableAndSwitchToItByLocator(driver, pages): + pages.load("blank.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'myFrame'))) + driver.execute_script("setTimeout(function(){var f = document.createElement('iframe'); f.id='myFrame'; f.src = '" + pages.url('iframeWithAlert.html') + "'; document.body.appendChild(f)}, 200)") + WebDriverWait(driver, 1).until(EC.frame_to_be_available_and_switch_to_it((By.ID, 'myFrame'))) + assert 'click me' == driver.find_element_by_id('alertInFrame').text + + +def testExpectedConditionInvisiblityOfElement(driver, pages): + pages.load("javascriptPage.html") + target = driver.find_element_by_id('clickToHide') + driver.execute_script("delayedShowHide(0, true)") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.invisibility_of_element(target)) + driver.execute_script("delayedShowHide(200, false)") + element = WebDriverWait(driver, 0.7).until(EC.invisibility_of_element(target)) + assert element.is_displayed() is False + assert target == element + + +def testExpectedConditionInvisiblityOfElementLocated(driver, pages): + pages.load("javascriptPage.html") + driver.execute_script("delayedShowHide(0, true)") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.invisibility_of_element_located((By.ID, 'clickToHide'))) + driver.execute_script("delayedShowHide(200, false)") + element = WebDriverWait(driver, 0.7).until(EC.invisibility_of_element_located((By.ID, 'clickToHide'))) + assert element.is_displayed() is False + + +def testExpectedConditionElementToBeClickable(driver, pages): + pages.load("javascriptPage.html") + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.element_to_be_clickable((By.ID, 'clickToHide'))) + driver.execute_script("delayedShowHide(200, true)") + WebDriverWait(driver, 0.7).until(EC.element_to_be_clickable((By.ID, 'clickToHide'))) + element = driver.find_element_by_id('clickToHide') + element.click() + WebDriverWait(driver, 3.5).until(EC.invisibility_of_element_located((By.ID, 'clickToHide'))) + assert element.is_displayed() is False + + +def testExpectedConditionStalenessOf(driver, pages): + pages.load('dynamicallyModifiedPage.html') + element = driver.find_element_by_id('element-to-remove') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.staleness_of(element)) + driver.find_element_by_id('buttonDelete').click() + assert 'element' == element.text + WebDriverWait(driver, 0.7).until(EC.staleness_of(element)) + with pytest.raises(StaleElementReferenceException): + element.text + + +def testExpectedConditionElementToBeSelected(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id('checky') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.element_to_be_selected(element)) + driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)") + WebDriverWait(driver, 0.7).until(EC.element_to_be_selected(element)) + assert element.is_selected() is True + + +def testExpectedConditionElementLocatedToBeSelected(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id('checky') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.element_located_to_be_selected((By.ID, 'checky'))) + driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)") + WebDriverWait(driver, 0.7).until(EC.element_located_to_be_selected((By.ID, 'checky'))) + assert element.is_selected() is True + + +def testExpectedConditionElementSelectionStateToBe(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id('checky') + WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, False)) + assert element.is_selected() is False + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, True)) + driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)") + WebDriverWait(driver, 0.7).until(EC.element_selection_state_to_be(element, True)) + assert element.is_selected() is True + + +def testExpectedConditionElementLocatedSelectionStateToBe(driver, pages): + pages.load("formPage.html") + element = driver.find_element_by_id('checky') + WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), False)) + assert element.is_selected() is False + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), True)) + driver.execute_script("setTimeout(function(){document.getElementById('checky').checked = true}, 200)") + WebDriverWait(driver, 0.7).until(EC.element_located_selection_state_to_be((By.ID, 'checky'), True)) + assert element.is_selected() is True + + +def testExpectedConditionAlertIsPresent(driver, pages): + pages.load('blank.html') + with pytest.raises(TimeoutException): + WebDriverWait(driver, 0.7).until(EC.alert_is_present()) + driver.execute_script("setTimeout(function(){alert('alerty')}, 200)") + WebDriverWait(driver, 0.7).until(EC.alert_is_present()) + alert = driver.switch_to.alert + assert 'alerty' == alert.text + alert.dismiss() diff --git a/test/selenium/webdriver/common/webserver.py b/test/selenium/webdriver/common/webserver.py new file mode 100644 index 00000000..df916399 --- /dev/null +++ b/test/selenium/webdriver/common/webserver.py @@ -0,0 +1,159 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +"""A simple web server for testing purpose. +It serves the testing html pages that are needed by the webdriver unit tests.""" + +import logging +import os +import socket +import threading +from io import open +try: + from urllib import request as urllib_request +except ImportError: + import urllib as urllib_request +try: + from http.server import BaseHTTPRequestHandler, HTTPServer + from socketserver import ThreadingMixIn +except ImportError: + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + from SocketServer import ThreadingMixIn + + +def updir(): + dirname = os.path.dirname + return dirname(dirname(__file__)) + + +LOGGER = logging.getLogger(__name__) +WEBDRIVER = os.environ.get("WEBDRIVER", updir()) +HTML_ROOT = os.path.join(WEBDRIVER, "../../../../common/src/web") +if not os.path.isdir(HTML_ROOT): + message = ("Can't find 'common_web' directory, try setting WEBDRIVER" + " environment variable WEBDRIVER:" + WEBDRIVER + " HTML_ROOT:" + HTML_ROOT) + LOGGER.error(message) + assert 0, message + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 8000 + + +class HtmlOnlyHandler(BaseHTTPRequestHandler): + """Http handler.""" + def do_GET(self): + """GET method handler.""" + try: + path = self.path[1:].split('?')[0] + if path[:5] == "page/": + html = """<html><head><title>Page{page_number}</title></head> + <body>Page number <span id=\"pageNumber\">{page_number}</span> + <p><a href=\"../xhtmlTest.html\" target=\"_top\">top</a> + </body></html>""".format(page_number=path[5:]) + html = html.encode('utf-8') + else: + with open(os.path.join(HTML_ROOT, path), 'r', encoding='latin-1') as f: + html = f.read().encode('utf-8') + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html) + except IOError: + self.send_error(404, 'File Not Found: %s' % path) + + def log_message(self, format, *args): + """Override default to avoid trashing stderr""" + pass + + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + pass + + +class SimpleWebServer(object): + """A very basic web server.""" + def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT): + self.stop_serving = False + host = host + port = port + while True: + try: + self.server = ThreadedHTTPServer((host, port), HtmlOnlyHandler) + self.host = host + self.port = port + break + except socket.error: + LOGGER.debug("port %d is in use, trying to next one" % port) + port += 1 + + self.thread = threading.Thread(target=self._run_web_server) + + def _run_web_server(self): + """Runs the server loop.""" + LOGGER.debug("web server started") + while not self.stop_serving: + self.server.handle_request() + self.server.server_close() + + def start(self): + """Starts the server.""" + self.thread.start() + + def stop(self): + """Stops the server.""" + self.stop_serving = True + try: + # This is to force stop the server loop + urllib_request.URLopener().open("http://%s:%d" % (self.host, self.port)) + except IOError: + pass + LOGGER.info("Shutting down the webserver") + self.thread.join() + + def where_is(self, path): + return "http://%s:%d/%s" % (self.host, self.port, path) + + +def main(argv=None): + from optparse import OptionParser + from time import sleep + + if argv is None: + import sys + argv = sys.argv + + parser = OptionParser("%prog [options]") + parser.add_option("-p", "--port", dest="port", type="int", + help="port to listen (default: %s)" % DEFAULT_PORT, + default=DEFAULT_PORT) + + opts, args = parser.parse_args(argv[1:]) + if args: + parser.error("wrong number of arguments") # Will exit + + server = SimpleWebServer(opts.port) + server.start() + print("Server started on port %s, hit CTRL-C to quit" % opts.port) + try: + while 1: + sleep(0.1) + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/test/selenium/webdriver/common/window_switching_tests.py b/test/selenium/webdriver/common/window_switching_tests.py new file mode 100644 index 00000000..8ce87f6e --- /dev/null +++ b/test/selenium/webdriver/common/window_switching_tests.py @@ -0,0 +1,205 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import NoSuchWindowException +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.common.by import By +from selenium.webdriver.common.window import WindowTypes +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +@pytest.fixture(autouse=True) +def close_windows(driver): + main_windows_handle = driver.current_window_handle + yield + try: + from urllib import request as url_request + except ImportError: + import urllib2 as url_request + URLError = url_request.URLError + + try: + window_handles = driver.window_handles + except URLError: + return + for handle in window_handles: + if handle != main_windows_handle: + driver.switch_to.window(handle) + driver.close() + driver.switch_to.window(main_windows_handle) + + +def testShouldSwitchFocusToANewWindowWhenItIsOpenedAndNotStopFutureOperations(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + + driver.find_element_by_link_text("Open new window").click() + assert driver.title == "XHTML Test Page" + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + assert driver.title == "We Arrive Here" + + pages.load("iframes.html") + handle = driver.current_window_handle + driver.find_element_by_id("iframe_page_heading") + driver.switch_to.frame(driver.find_element(By.ID, "iframe1")) + assert driver.current_window_handle == handle + + +def testCanSwitchToWindowByName(driver, pages): + pages.load("xhtmlTest.html") + handles = driver.window_handles + driver.find_element(By.LINK_TEXT, "Open new window").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + driver.switch_to.window("result") + assert driver.title == "We Arrive Here" + + +def testShouldThrowNoSuchWindowException(driver, pages): + pages.load("xhtmlTest.html") + with pytest.raises(NoSuchWindowException): + driver.switch_to.window("invalid name") + + +def testShouldThrowNoSuchWindowExceptionOnAnAttemptToGetItsHandle(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element(By.LINK_TEXT, "Open new window").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + driver.close() + + with pytest.raises(NoSuchWindowException): + driver.current_window_handle + + +@pytest.mark.xfail_ie +def testShouldThrowNoSuchWindowExceptionOnAnyOperationIfAWindowIsClosed(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element(By.LINK_TEXT, "Open new window").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + driver.close() + + with pytest.raises(NoSuchWindowException): + driver.title + + with pytest.raises(NoSuchWindowException): + driver.find_element_by_tag_name("body") + + +@pytest.mark.xfail_ie +def testShouldThrowNoSuchWindowExceptionOnAnyElementOperationIfAWindowIsClosed(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element(By.LINK_TEXT, "Open new window").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + element = driver.find_element_by_tag_name("body") + driver.close() + + with pytest.raises(NoSuchWindowException): + element.text + + +def testClickingOnAButtonThatClosesAnOpenWindowDoesNotCauseTheBrowserToHang(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element_by_name("windowThree").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + driver.find_element_by_id("close").click() + driver.switch_to.window(current) + driver.find_element_by_id("linkId") + + +def testCanCallGetWindowHandlesAfterClosingAWindow(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element_by_name("windowThree").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + + driver.find_element_by_id("close").click() + WebDriverWait(driver, 3).until(EC.number_of_windows_to_be(1)) + + +def testCanObtainAWindowHandle(driver, pages): + pages.load("xhtmlTest.html") + currentHandle = driver.current_window_handle + assert currentHandle is not None + + +def testFailingToSwitchToAWindowLeavesTheCurrentWindowAsIs(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + with pytest.raises(NoSuchWindowException): + driver.switch_to.window("I will never exist") + new_handle = driver.current_window_handle + assert current == new_handle + + +def testThatAccessingFindingAnElementAfterWindowIsClosedAndHaventswitchedDoesntCrash(driver, pages): + pages.load("xhtmlTest.html") + current = driver.current_window_handle + handles = driver.window_handles + driver.find_element_by_name("windowThree").click() + WebDriverWait(driver, 3).until(EC.new_window_is_opened(handles)) + handles = driver.window_handles + handles.remove(current) + driver.switch_to.window(handles[0]) + + with pytest.raises(WebDriverException): + driver.find_element_by_id("close").click() + all_handles = driver.window_handles + assert 1 == len(all_handles) + driver.find_element_by_id("close") + driver.switch_to.window(current) + + +@pytest.mark.xfail_ie +@pytest.mark.xfail_chrome +def testShouldBeAbleToCreateANewWindow(driver, pages): + original_handle = driver.current_window_handle + + driver.switch_to.new_window(WindowTypes.TAB) + new_handle = driver.current_window_handle + + driver.close() + driver.switch_to.window(original_handle) + + assert new_handle != original_handle diff --git a/test/selenium/webdriver/common/window_tests.py b/test/selenium/webdriver/common/window_tests.py new file mode 100644 index 00000000..11eeb2a9 --- /dev/null +++ b/test/selenium/webdriver/common/window_tests.py @@ -0,0 +1,160 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import os +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.support.wait import WebDriverWait + + +@pytest.mark.xfail_ie +@pytest.mark.xfail_chrome(reason="Fails on Travis") +@pytest.mark.xfail_marionette(reason="Fails on Travis") +@pytest.mark.xfail_firefox(reason="Fails on Travis") +@pytest.mark.xfail_remote(reason="Fails on Travis") +def testShouldMaximizeTheWindow(driver): + resize_timeout = 5 + wait = WebDriverWait(driver, resize_timeout) + old_size = driver.get_window_size() + driver.set_window_size(200, 200) + wait.until( + lambda dr: dr.get_window_size() != old_size if old_size["width"] != 200 and old_size["height"] != 200 else True) + size = driver.get_window_size() + driver.maximize_window() + wait.until(lambda dr: dr.get_window_size() != size) + new_size = driver.get_window_size() + assert new_size["width"] > size["width"] + assert new_size["height"] > size["height"] + + +def test_should_get_the_size_of_the_current_window(driver): + size = driver.get_window_size() + assert size.get('width') > 0 + assert size.get('height') > 0 + + +def test_should_set_the_size_of_the_current_window(driver): + size = driver.get_window_size() + + target_width = size.get('width') - 20 + target_height = size.get('height') - 20 + driver.set_window_size(width=target_width, height=target_height) + + new_size = driver.get_window_size() + assert new_size.get('width') == target_width + assert new_size.get('height') == target_height + + +def test_should_get_the_position_of_the_current_window(driver): + position = driver.get_window_position() + assert position.get('x') >= 0 + assert position.get('y') >= 0 + + +def test_should_set_the_position_of_the_current_window(driver): + position = driver.get_window_position() + + target_x = position.get('x') + 10 + target_y = position.get('y') + 10 + driver.set_window_position(x=target_x, y=target_y) + + WebDriverWait(driver, 2).until(lambda d: d.get_window_position()['x'] != position['x'] and + d.get_window_position()['y'] != position['y']) + + new_position = driver.get_window_position() + assert new_position.get('x') == target_x + assert new_position.get('y') == target_y + + +@pytest.mark.xfail_firefox(raises=WebDriverException, + reason='Get Window Rect command not implemented') +@pytest.mark.xfail_safari(raises=WebDriverException, + reason='Get Window Rect command not implemented') +def test_should_get_the_rect_of_the_current_window(driver): + rect = driver.get_window_rect() + assert rect.get('x') >= 0 + assert rect.get('y') >= 0 + assert rect.get('width') >= 0 + assert rect.get('height') >= 0 + + +@pytest.mark.xfail_firefox(raises=WebDriverException, + reason='Get Window Rect command not implemented') +@pytest.mark.xfail_safari(raises=WebDriverException, + reason='Get Window Rect command not implemented') +def test_should_set_the_rect_of_the_current_window(driver): + rect = driver.get_window_rect() + + target_x = rect.get('x') + 10 + target_y = rect.get('y') + 10 + target_width = rect.get('width') + 10 + target_height = rect.get('height') + 10 + + driver.set_window_rect(x=target_x, y=target_y, width=target_width, height=target_height) + + WebDriverWait(driver, 2).until(lambda d: d.get_window_position()['x'] != rect['x'] and + d.get_window_position()['y'] != rect['y']) + + new_rect = driver.get_window_rect() + + assert new_rect.get('x') == target_x + assert new_rect.get('y') == target_y + assert new_rect.get('width') == target_width + assert new_rect.get('height') == target_height + + +@pytest.mark.xfail_chrome(raises=WebDriverException, + reason='Fullscreen command not implemented') +@pytest.mark.xfail_firefox(raises=WebDriverException, + reason='Fullscreen command not implemented') +@pytest.mark.xfail_safari(raises=WebDriverException, + reason='Fullscreen command not implemented') +@pytest.mark.skipif(os.environ.get('CI') == 'true', + reason='Fullscreen command causes Travis to hang') +def test_should_fullscreen_the_current_window(driver): + start_width = driver.execute_script('return window.innerWidth;') + start_height = driver.execute_script('return window.innerHeight;') + + driver.fullscreen_window() + + WebDriverWait(driver, 2).until(lambda d: driver.execute_script('return window.innerWidth;') > + start_width) + + end_width = driver.execute_script('return window.innerWidth;') + end_height = driver.execute_script('return window.innerHeight;') + + driver.fullscreen_window() # Restore to original size + + assert end_width > start_width + assert end_height > start_height + + +@pytest.mark.xfail_chrome(raises=WebDriverException, + reason='Minimize command not implemented') +@pytest.mark.xfail_firefox(raises=WebDriverException, + reason='Minimize command not implemented') +@pytest.mark.xfail_safari(raises=WebDriverException, + reason='Minimize command not implemented') +@pytest.mark.skipif(os.environ.get('CI') == 'true', + reason='Minimize command causes Travis to hang') +@pytest.mark.no_driver_after_test +def test_should_minimize_the_current_window(driver): + driver.minimize_window() + minimized = driver.execute_script('return document.hidden;') + driver.quit() # Kill driver so we aren't running minimized after + + assert minimized is True diff --git a/test/selenium/webdriver/firefox/__init__.py b/test/selenium/webdriver/firefox/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/firefox/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/firefox/conftest.py b/test/selenium/webdriver/firefox/conftest.py new file mode 100644 index 00000000..2a842147 --- /dev/null +++ b/test/selenium/webdriver/firefox/conftest.py @@ -0,0 +1,33 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver import Firefox + + +@pytest.fixture +def capabilities(): + capabilities = {'marionette': False} + return capabilities + + +@pytest.fixture +def driver(capabilities): + driver = Firefox(capabilities=capabilities) + yield driver + driver.quit() diff --git a/test/selenium/webdriver/firefox/ff_launcher_tests.py b/test/selenium/webdriver/firefox/ff_launcher_tests.py new file mode 100644 index 00000000..23d53894 --- /dev/null +++ b/test/selenium/webdriver/firefox/ff_launcher_tests.py @@ -0,0 +1,37 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Firefox + + +def test_we_can_launch_multiple_firefox_instances(capabilities): + driver1 = Firefox(capabilities=capabilities) + driver2 = Firefox(capabilities=capabilities) + driver3 = Firefox(capabilities=capabilities) + driver1.quit() + driver2.quit() + driver3.quit() + + +def test_launch_firefox_with_none_service_log_path(capabilities): + driver = Firefox(capabilities=capabilities, service_log_path=None) + driver.quit() + + +def test_launch_firefox_with_empty_string_service_log_path(capabilities): + driver = Firefox(capabilities=capabilities, service_log_path="") + driver.quit() diff --git a/test/selenium/webdriver/firefox/ff_profile_tests.py b/test/selenium/webdriver/firefox/ff_profile_tests.py new file mode 100755 index 00000000..c88a3029 --- /dev/null +++ b/test/selenium/webdriver/firefox/ff_profile_tests.py @@ -0,0 +1,167 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import os +import zipfile + +try: + from io import BytesIO +except ImportError: + from cStringIO import StringIO as BytesIO + +try: + unicode +except NameError: + unicode = str + +from selenium.webdriver import Firefox, FirefoxProfile + + +def test_that_we_can_accept_a_profile(capabilities, webserver): + profile1 = FirefoxProfile() + profile1.set_preference("browser.startup.homepage_override.mstone", "") + profile1.set_preference("startup.homepage_welcome_url", webserver.where_is('simpleTest.html')) + profile1.update_preferences() + + profile2 = FirefoxProfile(profile1.path) + driver = Firefox( + capabilities=capabilities, + firefox_profile=profile2) + title = driver.title + driver.quit() + assert "Hello WebDriver" == title + + +def test_that_prefs_are_written_in_the_correct_format(): + profile = FirefoxProfile() + profile.set_preference("sample.preference", "hi there") + profile.update_preferences() + + assert 'hi there' == profile.default_preferences["sample.preference"] + + encoded = profile.encoded + decoded = base64.b64decode(encoded) + with BytesIO(decoded) as fp: + zip = zipfile.ZipFile(fp, "r") + for entry in zip.namelist(): + if entry.endswith("user.js"): + user_js = zip.read(entry) + for line in user_js.splitlines(): + if line.startswith(b'user_pref("sample.preference",'): + assert line.endswith(b'hi there");') + # there should be only one user.js + break + + +def test_that_unicode_prefs_are_written_in_the_correct_format(): + profile = FirefoxProfile() + profile.set_preference('sample.preference.2', unicode('hi there')) + profile.update_preferences() + + assert 'hi there' == profile.default_preferences["sample.preference.2"] + + encoded = profile.encoded + decoded = base64.b64decode(encoded) + with BytesIO(decoded) as fp: + zip = zipfile.ZipFile(fp, "r") + for entry in zip.namelist(): + if entry.endswith('user.js'): + user_js = zip.read(entry) + for line in user_js.splitlines(): + if line.startswith(b'user_pref("sample.preference.2",'): + assert line.endswith(b'hi there");') + # there should be only one user.js + break + + +def test_that_integer_prefs_are_written_in_the_correct_format(): + profile = FirefoxProfile() + profile.set_preference("sample.int.preference", 12345) + profile.update_preferences() + assert 12345 == profile.default_preferences["sample.int.preference"] + + +def test_that_boolean_prefs_are_written_in_the_correct_format(): + profile = FirefoxProfile() + profile.set_preference("sample.bool.preference", True) + profile.update_preferences() + assert profile.default_preferences["sample.bool.preference"] is True + + +def test_that_we_delete_the_profile(capabilities): + driver = Firefox(capabilities=capabilities) + path = driver.firefox_profile.path + driver.quit() + assert not os.path.exists(path) + + +def test_profiles_do_not_share_preferences(): + profile1 = FirefoxProfile() + profile1.accept_untrusted_certs = False + profile2 = FirefoxProfile() + # Default is true. Should remain so. + assert profile2.default_preferences["webdriver_accept_untrusted_certs"] is True + + +def test_add_extension_web_extension_with_id(capabilities, webserver): + current_directory = os.path.dirname(os.path.realpath(__file__)) + root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..') + # TODO: This file should probably live in a common directory. + extension_path = os.path.join(root_directory, 'javascript', 'node', 'selenium-webdriver', + 'lib', 'test', 'data', 'firefox', 'webextension.xpi') + + profile = FirefoxProfile() + profile.add_extension(extension_path) + + driver = Firefox(capabilities=capabilities, firefox_profile=profile) + profile_path = driver.firefox_profile.path + extension_path_in_profile = os.path.join(profile_path, 'extensions', 'webextensions-selenium-example@example.com') + assert os.path.exists(extension_path_in_profile) + driver.get(webserver.where_is('simpleTest.html')) + driver.find_element_by_id('webextensions-selenium-example') + driver.quit() + + +def test_add_extension_web_extension_without_id(capabilities, webserver): + current_directory = os.path.dirname(os.path.realpath(__file__)) + root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..') + extension_path = os.path.join(root_directory, 'third_party', 'firebug', 'mooltipass-1.1.87.xpi') + + profile = FirefoxProfile() + profile.add_extension(extension_path) + + driver = Firefox(capabilities=capabilities, firefox_profile=profile) + profile_path = driver.firefox_profile.path + extension_path_in_profile = os.path.join(profile_path, 'extensions', 'MooltipassExtension@1.1.87') + assert os.path.exists(extension_path_in_profile) + driver.quit() + + +def test_add_extension_legacy_extension(capabilities, webserver): + current_directory = os.path.dirname(os.path.realpath(__file__)) + root_directory = os.path.join(current_directory, '..', '..', '..', '..', '..') + extension_path = os.path.join(root_directory, 'third_party', 'firebug', 'firebug-1.5.0-fx.xpi') + + profile = FirefoxProfile() + profile.add_extension(extension_path) + + driver = Firefox(capabilities=capabilities, firefox_profile=profile) + profile_path = driver.firefox_profile.path + extension_path_in_profile = os.path.join(profile_path, 'extensions', 'firebug@software.joehewitt.com') + assert os.path.exists(extension_path_in_profile) + driver.quit() diff --git a/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py b/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py new file mode 100644 index 00000000..01c1d435 --- /dev/null +++ b/test/selenium/webdriver/firefox/ff_takes_full_page_screenshots_tests.py @@ -0,0 +1,31 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import base64 +import imghdr + + +def test_get_full_page_screenshot_as_base64(driver, pages): + pages.load("simpleTest.html") + result = base64.b64decode(driver.get_full_page_screenshot_as_base64()) + assert imghdr.what('', result) == 'png' + + +def test_get_full_page_screenshot_as_png(driver, pages): + pages.load("simpleTest.html") + result = driver.get_full_page_screenshot_as_png() + assert imghdr.what('', result) == 'png' diff --git a/test/selenium/webdriver/ie/__init__.py b/test/selenium/webdriver/ie/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/ie/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/ie/ie_launcher_tests.py b/test/selenium/webdriver/ie/ie_launcher_tests.py new file mode 100755 index 00000000..88d627f0 --- /dev/null +++ b/test/selenium/webdriver/ie/ie_launcher_tests.py @@ -0,0 +1,52 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Ie +from selenium.webdriver.ie.options import Options +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +def test_launch_and_close_browser(): + driver = Ie() + driver.quit() + + +def test_we_can_launch_multiple_ie_instances(): + driver1 = Ie() + driver2 = Ie() + driver3 = Ie() + driver1.quit() + driver2.quit() + driver3.quit() + + +def test_launch_ie_do_not_affect_default_capabilities(): + expected = DesiredCapabilities.INTERNETEXPLORER.copy() + driver = Ie() + actual = DesiredCapabilities.INTERNETEXPLORER.copy() + driver.quit() + assert actual == expected + + +def test_launch_ie_with_options(pages): + opts = Options() + expected = "clicks.html" + opts.initial_browser_url = pages.url(expected) + driver = Ie(options=opts) + actual = driver.current_url + driver.quit() + assert expected in actual diff --git a/test/selenium/webdriver/marionette/__init__.py b/test/selenium/webdriver/marionette/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/marionette/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/marionette/conftest.py b/test/selenium/webdriver/marionette/conftest.py new file mode 100644 index 00000000..77871a7e --- /dev/null +++ b/test/selenium/webdriver/marionette/conftest.py @@ -0,0 +1,43 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver import Firefox + + +@pytest.fixture +def capabilities(): + capabilities = {'marionette': True} + return capabilities + + +@pytest.fixture +def driver_class(): + return Firefox + + +@pytest.fixture +def driver_kwargs(capabilities): + return {'capabilities': capabilities} + + +@pytest.fixture +def driver(driver_class, driver_kwargs): + driver = driver_class(**driver_kwargs) + yield driver + driver.quit() diff --git a/test/selenium/webdriver/marionette/mn_binary_tests.py b/test/selenium/webdriver/marionette/mn_binary_tests.py new file mode 100644 index 00000000..5c8efde1 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_binary_tests.py @@ -0,0 +1,35 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import WebDriverException +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary + + +@pytest.fixture(params=['foo', FirefoxBinary(firefox_path='foo')], + ids=['string', 'binary']) +def driver_kwargs(request, driver_kwargs): + driver_kwargs['firefox_binary'] = request.param + return driver_kwargs + + +@pytest.mark.xfail(reason='https://github.com/mozilla/geckodriver/issues/832') +def test_invalid_binary(driver_class, driver_kwargs): + with pytest.raises(WebDriverException) as excinfo: + driver_class(**driver_kwargs) + assert 'no such file or directory' in str(excinfo.value) diff --git a/test/selenium/webdriver/marionette/mn_context_tests.py b/test/selenium/webdriver/marionette/mn_context_tests.py new file mode 100644 index 00000000..8722e98c --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_context_tests.py @@ -0,0 +1,27 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def test_context_sets_correct_context_and_returns(driver): + + def get_context(): + return driver.execute('GET_CONTEXT').pop('value') + + assert get_context() == driver.CONTEXT_CONTENT + with driver.context(driver.CONTEXT_CHROME): + assert get_context() == driver.CONTEXT_CHROME + assert get_context() == driver.CONTEXT_CONTENT diff --git a/test/selenium/webdriver/marionette/mn_launcher_tests.py b/test/selenium/webdriver/marionette/mn_launcher_tests.py new file mode 100644 index 00000000..e81f0258 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_launcher_tests.py @@ -0,0 +1,41 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver import Firefox + + +def test_launch_and_close_browser(driver): + assert 'browserName' in driver.capabilities + + +def test_we_can_launch_multiple_firefox_instances(capabilities): + driver1 = Firefox(capabilities=capabilities) + driver2 = Firefox(capabilities=capabilities) + driver3 = Firefox(capabilities=capabilities) + driver1.quit() + driver2.quit() + driver3.quit() + + +def test_launch_firefox_with_none_service_log_path(capabilities): + driver = Firefox(capabilities=capabilities, service_log_path=None) + driver.quit() + + +def test_launch_firefox_with_empty_string_service_log_path(capabilities): + driver = Firefox(capabilities=capabilities, service_log_path="") + driver.quit() diff --git a/test/selenium/webdriver/marionette/mn_options_tests.py b/test/selenium/webdriver/marionette/mn_options_tests.py new file mode 100644 index 00000000..33e988b2 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_options_tests.py @@ -0,0 +1,132 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +try: + basestring +except NameError: # Python 3.x + basestring = str + +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary +from selenium.webdriver.firefox.firefox_profile import FirefoxProfile +from selenium.webdriver.firefox.options import Log, Options + + +@pytest.fixture +def driver_kwargs(driver_kwargs): + driver_kwargs['options'] = Options() + return driver_kwargs + + +class TestIntegration(object): + def test_we_can_pass_options(self, driver, pages): + pages.load("formPage.html") + driver.find_element_by_id("cheese") + + +class TestUnit(object): + def test_ctor(self): + opts = Options() + assert opts._binary is None + assert opts._preferences == {} + assert opts._profile is None + assert opts._arguments == [] + assert isinstance(opts.log, Log) + + def test_binary(self): + opts = Options() + assert opts.binary is None + + other_binary = FirefoxBinary() + assert other_binary != opts.binary + opts.binary = other_binary + assert other_binary == opts.binary + + path = "/path/to/binary" + opts.binary = path + assert isinstance(opts.binary, FirefoxBinary) + assert opts.binary._start_cmd == path + + def test_prefs(self): + opts = Options() + assert len(opts.preferences) == 0 + assert isinstance(opts.preferences, dict) + + opts.set_preference("spam", "ham") + assert len(opts.preferences) == 1 + opts.set_preference("eggs", True) + assert len(opts.preferences) == 2 + opts.set_preference("spam", "spam") + assert len(opts.preferences) == 2 + assert opts.preferences == {"spam": "spam", "eggs": True} + + def test_profile(self, tmpdir_factory): + opts = Options() + assert opts.profile is None + + other_profile = FirefoxProfile() + assert other_profile != opts.profile + opts.profile = other_profile + assert other_profile == opts.profile + + opts.profile = str(tmpdir_factory.mktemp("profile")) + assert isinstance(opts.profile, FirefoxProfile) + + def test_arguments(self): + opts = Options() + assert len(opts.arguments) == 0 + + opts.add_argument("--foo") + assert len(opts.arguments) == 1 + opts.arguments.append("--bar") + assert len(opts.arguments) == 2 + assert opts.arguments == ["--foo", "--bar"] + + def test_to_capabilities(self): + opts = Options() + assert opts.to_capabilities() == DesiredCapabilities.FIREFOX + + profile = FirefoxProfile() + opts.profile = profile + caps = opts.to_capabilities() + assert "moz:firefoxOptions" in caps + assert "profile" in caps["moz:firefoxOptions"] + assert isinstance(caps["moz:firefoxOptions"]["profile"], basestring) + assert caps["moz:firefoxOptions"]["profile"] == profile.encoded + + opts.add_argument("--foo") + caps = opts.to_capabilities() + assert "moz:firefoxOptions" in caps + assert "args" in caps["moz:firefoxOptions"] + assert caps["moz:firefoxOptions"]["args"] == ["--foo"] + + binary = FirefoxBinary() + opts.binary = binary + caps = opts.to_capabilities() + assert "moz:firefoxOptions" in caps + assert "binary" in caps["moz:firefoxOptions"] + assert isinstance(caps["moz:firefoxOptions"]["binary"], basestring) + assert caps["moz:firefoxOptions"]["binary"] == binary._start_cmd + + opts.set_preference("spam", "ham") + caps = opts.to_capabilities() + assert "moz:firefoxOptions" in caps + assert "prefs" in caps["moz:firefoxOptions"] + assert isinstance(caps["moz:firefoxOptions"]["prefs"], dict) + assert caps["moz:firefoxOptions"]["prefs"]["spam"] == "ham" diff --git a/test/selenium/webdriver/marionette/mn_preferences_tests.py b/test/selenium/webdriver/marionette/mn_preferences_tests.py new file mode 100644 index 00000000..21502263 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_preferences_tests.py @@ -0,0 +1,33 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.firefox.options import Options + + +@pytest.fixture +def driver_kwargs(request, driver_kwargs): + options = Options() + options.set_preference('browser.startup.homepage_override.mstone', '') + options.set_preference('startup.homepage_welcome_url', 'about:') + driver_kwargs['options'] = options + return driver_kwargs + + +def test_preferences_are_used(driver): + assert 'about:' == driver.current_url diff --git a/test/selenium/webdriver/marionette/mn_profile_tests.py b/test/selenium/webdriver/marionette/mn_profile_tests.py new file mode 100644 index 00000000..4b0895bb --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_profile_tests.py @@ -0,0 +1,57 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os + +import pytest + +from selenium.webdriver import FirefoxProfile +from selenium.webdriver.firefox.options import Options + + +@pytest.fixture(params=['capabilities', 'firefox_profile', 'options']) +def driver_kwargs(request, driver_kwargs, profile): + if request.param == 'capabilities': + options = {'profile': profile} + driver_kwargs[request.param].setdefault('moz:firefoxOptions', options) + elif request.param == 'firefox_profile': + driver_kwargs[request.param] = profile + elif request.param == 'options': + options = Options() + options.profile = profile + driver_kwargs[request.param] = options + driver_kwargs['firefox_profile'] = profile + return driver_kwargs + + +@pytest.fixture +def profile(): + profile = FirefoxProfile() + profile.set_preference('browser.startup.homepage_override.mstone', '') + profile.set_preference('startup.homepage_welcome_url', 'about:') + profile.update_preferences() + return profile + + +def test_profile_is_used(driver): + assert 'about:' == driver.current_url + + +def test_profile_is_deleted(driver, profile): + assert os.path.exists(profile.path) + driver.quit() + assert not os.path.exists(profile.path) diff --git a/test/selenium/webdriver/marionette/mn_service_tests.py b/test/selenium/webdriver/marionette/mn_service_tests.py new file mode 100644 index 00000000..b3f783b7 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_service_tests.py @@ -0,0 +1,32 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.firefox.service import Service + + +def test_command_line_args(): + service = Service("geckodriver", service_args=["--log", "trace"]) + found = False + + args = service.command_line_args() + + for idx in range(len(args) - 1): + if args[idx] == "--log" and args[idx + 1] == "trace": + found = True + break + + assert found, "Provided arguments do not exist in array" diff --git a/test/selenium/webdriver/marionette/mn_set_context_tests.py b/test/selenium/webdriver/marionette/mn_set_context_tests.py new file mode 100644 index 00000000..6a64a713 --- /dev/null +++ b/test/selenium/webdriver/marionette/mn_set_context_tests.py @@ -0,0 +1,21 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def test_we_can_switch_context_to_chrome(driver): + driver.set_context('chrome') + assert 1 == driver.execute_script("var c = Components.classes; return 1;") diff --git a/test/selenium/webdriver/remote/__init__.py b/test/selenium/webdriver/remote/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/remote/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/remote/remote_firefox_profile_tests.py b/test/selenium/webdriver/remote/remote_firefox_profile_tests.py new file mode 100644 index 00000000..5e60db95 --- /dev/null +++ b/test/selenium/webdriver/remote/remote_firefox_profile_tests.py @@ -0,0 +1,45 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest +from selenium import webdriver +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities + + +@pytest.fixture +def capabilities(): + capabilities = DesiredCapabilities.FIREFOX.copy() + capabilities['marionette'] = False + return capabilities + + +@pytest.fixture +def driver(options): + driver = webdriver.Remote(options=options) + yield driver + driver.quit() + + +@pytest.fixture +def options(): + options = webdriver.FirefoxOptions() + options.set_preference('browser.startup.homepage', 'about:') + return options + + +def test_profile_is_used(driver): + assert 'about:blank' == driver.current_url or 'about:' == driver.current_url diff --git a/test/selenium/webdriver/safari/conftest.py b/test/selenium/webdriver/safari/conftest.py new file mode 100644 index 00000000..6aa8968b --- /dev/null +++ b/test/selenium/webdriver/safari/conftest.py @@ -0,0 +1,37 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver import Safari + + +@pytest.fixture +def driver_class(): + return Safari + + +@pytest.fixture +def driver_kwargs(): + return {} + + +@pytest.fixture +def driver(driver_class, driver_kwargs): + driver = driver_class(**driver_kwargs) + yield driver + driver.quit() diff --git a/test/selenium/webdriver/safari/launcher_tests.py b/test/selenium/webdriver/safari/launcher_tests.py new file mode 100644 index 00000000..26601a6f --- /dev/null +++ b/test/selenium/webdriver/safari/launcher_tests.py @@ -0,0 +1,66 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import os + +import pytest + + +def test_launch(driver): + assert driver.capabilities['browserName'] == 'safari' + + +def test_launch_with_invalid_executable_path_raises_exception(driver_class): + path = '/this/path/should/never/exist' + assert not os.path.exists(path) + with pytest.raises(Exception) as e: + driver_class(executable_path=path) + assert 'SafariDriver requires Safari 10 on OSX El Capitan' in str(e) + + +class TestTechnologyPreview(object): + + @pytest.fixture + def driver_kwargs(self): + path = '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver' + assert os.path.exists(path), 'Safari Technology Preview required! Download it from https://developer.apple.com/safari/technology-preview/' + return {'executable_path': path} + + def test_launch(self, driver): + assert driver.capabilities['browserName'] == 'safari' + + +def test_launch_safari_with_legacy_flag(mocker, driver_class): + import subprocess + mocker.patch('subprocess.Popen') + try: + driver_class(service_args=['--legacy']) + except Exception as e: + pass + args, kwargs = subprocess.Popen.call_args + assert '--legacy' in args[0] + + +def test_launch_safari_without_legacy_flag(mocker, driver_class): + import subprocess + mocker.patch('subprocess.Popen') + try: + driver_class() + except Exception as e: + pass + args, kwargs = subprocess.Popen.call_args + assert '--legacy' not in args[0] diff --git a/test/selenium/webdriver/support/__init__.py b/test/selenium/webdriver/support/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/selenium/webdriver/support/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/selenium/webdriver/support/color_tests.py b/test/selenium/webdriver/support/color_tests.py new file mode 100644 index 00000000..1f36aec5 --- /dev/null +++ b/test/selenium/webdriver/support/color_tests.py @@ -0,0 +1,121 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from selenium.webdriver.support.color import Color + + +def test_rgb_to_rgb(): + rgb = "rgb(1, 2, 3)" + assert Color.from_string(rgb).rgb == rgb + + +def test_rgb_to_rgba(): + rgb = "rgb(1, 2, 3)" + assert Color.from_string(rgb).rgba == "rgba(1, 2, 3, 1)" + + +def test_rgb_pct_to_rgba(): + rgb = "rgb(10%, 20%, 30%)" + assert Color.from_string(rgb).rgba == "rgba(25, 51, 76, 1)" + + +def test_rgb_allows_whitespace(): + rgb = "rgb(\t1, 2 , 3)" + assert Color.from_string(rgb).rgb == "rgb(1, 2, 3)" + + +def test_rgba_to_rgba(): + rgba = "rgba(1, 2, 3, 0.5)" + assert Color.from_string(rgba).rgba == rgba + + +def test_rgba_pct_to_rgba(): + rgba = "rgba(10%, 20%, 30%, 0.5)" + assert Color.from_string(rgba).rgba == "rgba(25, 51, 76, 0.5)" + + +def test_hex_to_hex(): + hex_ = "#ff00a0" + assert Color.from_string(hex_).hex == hex_ + + +def test_hex_to_rgb(): + hex_ = "#01Ff03" + rgb = "rgb(1, 255, 3)" + assert Color.from_string(hex_).rgb == rgb + + +def test_hex_to_rgba(): + hex_ = "#01Ff03" + rgba = "rgba(1, 255, 3, 1)" + assert Color.from_string(hex_).rgba == rgba + + hex_ = "#00ff33" + rgba = "rgba(0, 255, 51, 1)" + assert Color.from_string(hex_).rgba == rgba + + +def test_rgb_to_hex(): + assert Color.from_string("rgb(1, 255, 3)").hex == "#01ff03" + + +def test_hex3_to_rgba(): + assert Color.from_string("#0f3").rgba == "rgba(0, 255, 51, 1)" + + +def test_hsl_to_rgba(): + hsl = "hsl(120, 100%, 25%)" + rgba = "rgba(0, 128, 0, 1)" + assert Color.from_string(hsl).rgba == rgba + + hsl = "hsl(100, 0%, 50%)" + rgba = "rgba(128, 128, 128, 1)" + assert Color.from_string(hsl).rgba == rgba + + +def test_hsla_to_rgba(): + hsla = "hsla(120, 100%, 25%, 1)" + rgba = "rgba(0, 128, 0, 1)" + assert Color.from_string(hsla).rgba == rgba + + hsla = "hsla(100, 0%, 50%, 0.5)" + rgba = "rgba(128, 128, 128, 0.5)" + assert Color.from_string(hsla).rgba == rgba + + +def test_named_color(): + assert Color.from_string("green").rgba == "rgba(0, 128, 0, 1)" + assert Color.from_string("gray").rgba == "rgba(128, 128, 128, 1)" + assert Color.from_string("aqua").hex == "#00ffff" + assert Color.from_string("transparent").rgba == "rgba(0, 0, 0, 0)" + + +def test_equals(): + assert Color.from_string("#f00") == Color.from_string("rgb(255, 0, 0)") + assert Color.from_string("rgba(30, 30, 30, 0.2)") != Color.from_string("rgba(30, 30, 30, 1)") + + +def test_hash(): + hash1 = hash(Color.from_string("#f00")) + hash2 = hash(Color.from_string("rgb(255, 0, 0)")) + assert hash1 == hash2 + + +def test_string_representations(): + hex_ = "#01Ff03" + assert str(Color.from_string(hex_)) == "Color: rgba(1, 255, 3, 1)" + assert repr(Color.from_string(hex_)) == "Color(red=1, green=255, blue=3, alpha=1)" diff --git a/test/selenium/webdriver/support/conftest.py b/test/selenium/webdriver/support/conftest.py new file mode 100644 index 00000000..75670927 --- /dev/null +++ b/test/selenium/webdriver/support/conftest.py @@ -0,0 +1,21 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +def pytest_generate_tests(metafunc): + if 'driver' in metafunc.fixturenames and metafunc.config.option.drivers: + metafunc.parametrize('driver', metafunc.config.option.drivers, indirect=True) diff --git a/test/selenium/webdriver/support/event_firing_webdriver_tests.py b/test/selenium/webdriver/support/event_firing_webdriver_tests.py new file mode 100644 index 00000000..e4ad8c01 --- /dev/null +++ b/test/selenium/webdriver/support/event_firing_webdriver_tests.py @@ -0,0 +1,246 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +try: + from io import BytesIO +except ImportError: + from cStringIO import StringIO as BytesIO + +import pytest + +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.common.by import By +from selenium.webdriver.support.events import EventFiringWebDriver, AbstractEventListener +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.actions.action_builder import ActionBuilder + + +@pytest.fixture +def log(): + log = BytesIO() + yield log + log.close() + + +def test_should_fire_navigation_events(driver, log, pages): + + class EventListener(AbstractEventListener): + + def before_navigate_to(self, url, driver): + log.write(("before_navigate_to %s" % url.split("/")[-1]).encode()) + + def after_navigate_to(self, url, driver): + log.write(("after_navigate_to %s" % url.split("/")[-1]).encode()) + + def before_navigate_back(self, driver): + log.write(b"before_navigate_back") + + def after_navigate_back(self, driver): + log.write(b"after_navigate_back") + + def before_navigate_forward(self, driver): + log.write(b"before_navigate_forward") + + def after_navigate_forward(self, driver): + log.write(b"after_navigate_forward") + + ef_driver = EventFiringWebDriver(driver, EventListener()) + ef_driver.get(pages.url("formPage.html")) + ef_driver.find_element(by=By.ID, value="imageButton").submit() + WebDriverWait(ef_driver, 5).until(lambda d: d.title == "We Arrive Here") + assert ef_driver.title == "We Arrive Here" + + ef_driver.back() + assert ef_driver.title == "We Leave From Here" + + ef_driver.forward() + assert ef_driver.title == "We Arrive Here" + + assert (b"before_navigate_to formPage.html" + b"after_navigate_to formPage.html" + b"before_navigate_back" + b"after_navigate_back" + b"before_navigate_forward" + b"after_navigate_forward") == log.getvalue() + + +def test_should_fire_click_event(driver, log, pages): + + class EventListener(AbstractEventListener): + + def before_click(self, element, driver): + log.write(b"before_click") + + def after_click(self, element, driver): + log.write(b"after_click") + + ef_driver = EventFiringWebDriver(driver, EventListener()) + ef_driver.get(pages.url("clicks.html")) + ef_driver.find_element(By.ID, "overflowLink").click() + assert ef_driver.title == "XHTML Test Page" + + assert b"before_click" + b"after_click" == log.getvalue() + + +def test_should_fire_change_value_event(driver, log, pages): + + class EventListener(AbstractEventListener): + + def before_change_value_of(self, element, driver): + log.write(b"before_change_value_of") + + def after_change_value_of(self, element, driver): + log.write(b"after_change_value_of") + + ef_driver = EventFiringWebDriver(driver, EventListener()) + ef_driver.get(pages.url("readOnlyPage.html")) + element = ef_driver.find_element_by_id("writableTextInput") + element.clear() + assert "" == element.get_attribute("value") + + ef_driver.get(pages.url("javascriptPage.html")) + keyReporter = ef_driver.find_element(by=By.ID, value="keyReporter") + keyReporter.send_keys("abc def") + assert keyReporter.get_attribute("value") == "abc def" + + assert (b"before_change_value_of" + b"after_change_value_of" + b"before_change_value_of" + b"after_change_value_of") == log.getvalue() + + +def test_should_fire_find_event(driver, log, pages): + + class EventListener(AbstractEventListener): + + def before_find(self, by, value, driver): + log.write(("before_find by %s %s" % (by, value)).encode()) + + def after_find(self, by, value, driver): + log.write(("after_find by %s %s" % (by, value)).encode()) + + ef_driver = EventFiringWebDriver(driver, EventListener()) + ef_driver.get(pages.url("simpleTest.html")) + e = ef_driver.find_element_by_id("oneline") + assert "A single line of text" == e.text + + e = ef_driver.find_element_by_xpath("/html/body/p[1]") + assert "A single line of text" == e.text + + ef_driver.get(pages.url("frameset.html")) + elements = ef_driver.find_elements_by_css_selector("frame#sixth") + assert 1 == len(elements) + assert "frame" == elements[0].tag_name.lower() + assert "sixth" == elements[0].get_attribute("id") + + assert (b"before_find by id oneline" + b"after_find by id oneline" + b"before_find by xpath /html/body/p[1]" + b"after_find by xpath /html/body/p[1]" + b"before_find by css selector frame#sixth" + b"after_find by css selector frame#sixth") == log.getvalue() + + +def test_should_call_listener_when_an_exception_is_thrown(driver, log, pages): + + class EventListener(AbstractEventListener): + def on_exception(self, exception, driver): + if isinstance(exception, NoSuchElementException): + log.write(b"NoSuchElementException is thrown") + + ef_driver = EventFiringWebDriver(driver, EventListener()) + ef_driver.get(pages.url("simpleTest.html")) + with pytest.raises(NoSuchElementException): + ef_driver.find_element(By.ID, "foo") + assert b"NoSuchElementException is thrown" == log.getvalue() + + +def test_should_unwrap_element_args_when_calling_scripts(driver, log, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + ef_driver.get(pages.url("javascriptPage.html")) + button = ef_driver.find_element_by_id("plainButton") + value = ef_driver.execute_script( + "arguments[0]['flibble'] = arguments[0].getAttribute('id'); return arguments[0]['flibble']", + button) + assert "plainButton" == value + + +def test_should_unwrap_element_args_when_switching_frames(driver, log, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + ef_driver.get(pages.url("iframes.html")) + frame = ef_driver.find_element_by_id("iframe1") + ef_driver.switch_to.frame(frame) + assert "click me!" == ef_driver.find_element_by_id("imageButton").get_attribute("alt") + + +def test_should_be_able_to_access_wrapped_instance_from_event_calls(driver): + + class EventListener(AbstractEventListener): + def before_navigate_to(url, d): + assert driver is d + + ef_driver = EventFiringWebDriver(driver, EventListener()) + wrapped_driver = ef_driver.wrapped_driver + assert driver is wrapped_driver + + +def test_using_kwargs(driver, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + ef_driver.get(pages.url("javascriptPage.html")) + ef_driver.get_cookie(name="cookie_name") + element = ef_driver.find_element_by_id("plainButton") + element.get_attribute(name="id") + + +def test_missing_attributes_raise_error(driver, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + + with pytest.raises(AttributeError): + ef_driver.attribute_should_not_exist + + ef_driver.get(pages.url("readOnlyPage.html")) + element = ef_driver.find_element_by_id("writableTextInput") + + with pytest.raises(AttributeError): + element.attribute_should_not_exist + + +def test_can_use_pointer_input_with_event_firing_webdriver(driver, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + pages.load("javascriptPage.html") + to_click = ef_driver.find_element_by_id("clickField") + + actions = ActionBuilder(ef_driver) + pointer = actions.pointer_action + pointer.move_to(to_click).click() + actions.perform() + + assert to_click.get_attribute('value') == 'Clicked' + + +def test_can_use_key_input_with_event_firing_webdriver(driver, pages): + ef_driver = EventFiringWebDriver(driver, AbstractEventListener()) + pages.load("javascriptPage.html") + ef_driver.find_element_by_id("keyUp").click() + + actions = ActionBuilder(ef_driver) + key = actions.key_action + key.send_keys('Success') + actions.perform() + + result = ef_driver.find_element_by_id("result") + assert result.text == 'Success' diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/unit/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/unit/selenium/__init__.py b/test/unit/selenium/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/unit/selenium/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/unit/selenium/webdriver/__init__.py b/test/unit/selenium/webdriver/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/unit/selenium/webdriver/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/unit/selenium/webdriver/chrome/chrome_options_tests.py b/test/unit/selenium/webdriver/chrome/chrome_options_tests.py new file mode 100644 index 00000000..f14f9968 --- /dev/null +++ b/test/unit/selenium/webdriver/chrome/chrome_options_tests.py @@ -0,0 +1,155 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +import platform +from os import path + +import pytest + +from selenium.webdriver.chrome.options import Options + + +@pytest.fixture +def options(): + return Options() + + +def test_set_binary_location(options): + options.binary_location = '/foo/bar' + assert options._binary_location == '/foo/bar' + + +def test_get_binary_location(options): + options._binary_location = '/foo/bar' + assert options.binary_location == '/foo/bar' + + +def test_set_debugger_address(options): + options.debugger_address = '/foo/bar' + assert options._debugger_address == '/foo/bar' + + +def test_get_debugger_address(options): + options._debugger_address = '/foo/bar' + assert options.debugger_address == '/foo/bar' + + +def test_add_arguments(options): + options.add_argument('foo') + assert 'foo' in options._arguments + + +def test_get_arguments(options): + options._arguments = ['foo'] + assert 'foo' in options.arguments + + +def test_raises_exception_if_argument_is_falsy(options): + with pytest.raises(ValueError): + options.add_argument(None) + + +def test_raises_exception_if_extension_is_falsy(options): + with pytest.raises(ValueError): + options.add_extension(None) + + +def test_raises_exception_if_extension_does_not_exist(options): + with pytest.raises(IOError): + options.add_extension(path.join(path.abspath(path.curdir), 'fakepath')) + + +def test_add_extension(options, mocker): + pth = path.abspath(path.expanduser('/foo/bar')) + mocker.patch('os.path.exists').return_value = True + options.add_extension(pth) + assert pth in options._extension_files + + +def test_raises_exception_if_encoded_extension_is_falsy(options): + with pytest.raises(ValueError): + options.add_encoded_extension(None) + + +def test_add_encoded_extension(options): + options.add_encoded_extension('/foo/bar') + assert '/foo/bar' in options._extensions + + +def test_get_extensions_from_extension_files(options, mocker): + null = 'NUL' if platform.system().lower() == 'windows' else '/dev/null' + mocker.patch( + 'selenium.webdriver.chrome.options.open'.format(__name__)).return_value = open(null) + mocker.patch('base64.b64encode').return_value = 'foo'.encode() + options._extension_files = ['foo'] + assert 'foo' in options.extensions + + +def test_get_extensions_from_encoded_extensions(options, mocker): + options._extensions = ['foo'] + assert 'foo' in options.extensions + + +def test_add_experimental_options(options): + options.add_experimental_option('foo', 'bar') + assert options._experimental_options['foo'] == 'bar' + + +def test_get_experimental_options(options): + options._experimental_options = {'foo': 'bar'} + assert options.experimental_options['foo'] == 'bar' + + +def test_set_headless(options): + options.headless = True + assert '--headless' in options._arguments + + +def test_unset_headless(options): + options._arguments = ['--headless'] + options.headless = False + assert '--headless' not in options._arguments + + +def test_get_headless(options): + options._arguments = ['--headless'] + assert options.headless is True + + +def test_creates_capabilities(options): + options._arguments = ['foo'] + options._binary_location = '/bar' + options._extensions = ['baz'] + options._debugger_address = '/foo/bar' + options._experimental_options = {'foo': 'bar'} + caps = options.to_capabilities() + opts = caps.get(Options.KEY) + assert opts + assert 'foo' in opts['args'] + assert opts['binary'] == '/bar' + assert 'baz' in opts['extensions'] + assert opts['debuggerAddress'] == '/foo/bar' + assert opts['foo'] == 'bar' + + +def test_starts_with_default_capabilities(options): + from selenium.webdriver import DesiredCapabilities + assert options._caps == DesiredCapabilities.CHROME + + +def test_is_a_baseoptions(options): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(options, BaseOptions) diff --git a/test/unit/selenium/webdriver/common/common_options_tests.py b/test/unit/selenium/webdriver/common/common_options_tests.py new file mode 100644 index 00000000..e921eef6 --- /dev/null +++ b/test/unit/selenium/webdriver/common/common_options_tests.py @@ -0,0 +1,35 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.common.options import ArgOptions + + +@pytest.fixture +def options(): + return ArgOptions() + + +def test_add_arguments(options): + options.add_argument('foo') + assert 'foo' in options._arguments + + +def test_get_arguments(options): + options._arguments = ['foo'] + assert 'foo' in options.arguments diff --git a/test/unit/selenium/webdriver/edge/edge_options_tests.py b/test/unit/selenium/webdriver/edge/edge_options_tests.py new file mode 100644 index 00000000..3d465779 --- /dev/null +++ b/test/unit/selenium/webdriver/edge/edge_options_tests.py @@ -0,0 +1,56 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.edge.options import Options + + +@pytest.fixture +def options(): + return Options() + + +def test_raises_exception_with_invalid_page_load_strategy(options): + with pytest.raises(ValueError): + options.page_load_strategy = 'never' + + +def test_set_page_load_strategy(options): + options.page_load_strategy = 'normal' + assert options._page_load_strategy == 'normal' + + +def test_get_page_load_strategy(options): + options._page_load_strategy = 'normal' + assert options.page_load_strategy == 'normal' + + +def test_creates_capabilities(options): + options.page_load_strategy = 'eager' + caps = options.to_capabilities() + assert caps['pageLoadStrategy'] == 'eager' + + +def test_starts_with_default_capabilities(options): + from selenium.webdriver import DesiredCapabilities + assert options._caps == DesiredCapabilities.EDGE + + +def test_is_a_baseoptions(options): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(options, BaseOptions) diff --git a/test/unit/selenium/webdriver/firefox/firefox_options_tests.py b/test/unit/selenium/webdriver/firefox/firefox_options_tests.py new file mode 100644 index 00000000..ba7c276a --- /dev/null +++ b/test/unit/selenium/webdriver/firefox/firefox_options_tests.py @@ -0,0 +1,162 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common.exceptions import InvalidArgumentException +from selenium.webdriver.common.proxy import Proxy, ProxyType +from selenium.webdriver.firefox.firefox_binary import FirefoxBinary +from selenium.webdriver.firefox.firefox_profile import FirefoxProfile +from selenium.webdriver.firefox.options import Options + + +@pytest.fixture +def options(): + return Options() + + +def test_set_binary_with_firefox_binary(options): + binary = FirefoxBinary('foo') + options.binary = binary + assert options._binary == binary + + +def test_set_binary_with_path(options): + options.binary = '/foo' + assert options._binary._start_cmd == '/foo' + + +def test_get_binary(options): + options.binary = '/foo' + assert options.binary._start_cmd == '/foo' + + +def test_set_binary_location(options): + options.binary_location = '/foo' + assert options._binary._start_cmd == '/foo' + + +def test_get_binary_location(options): + options._binary = FirefoxBinary('/foo') + assert options.binary_location == '/foo' + + +def test_set_preference(options): + options.set_preference('foo', 'bar') + assert options._preferences['foo'] == 'bar' + + +def test_get_preferences(options): + options._preferences = {'foo': 'bar'} + assert options.preferences['foo'] == 'bar' + + +def test_set_proxy(options): + proxy = Proxy({'proxyType': ProxyType.MANUAL}) + options.proxy = proxy + assert options._proxy == proxy + + +def test_raises_exception_if_proxy_is_not_proxy_object(options): + with pytest.raises(InvalidArgumentException): + options.proxy = 'foo' + + +def test_get_proxy(options): + options._proxy = 'foo' + assert options.proxy == 'foo' + + +def test_set_profile_with_firefox_profile(options): + profile = FirefoxProfile() + options.profile = profile + assert options._profile == profile + + +def test_set_profile_with_path(options): + options.profile = None + assert isinstance(options._profile, FirefoxProfile) + + +def test_get_profile(options): + options._profile = 'foo' + assert options.profile == 'foo' + + +def test_add_arguments(options): + options.add_argument('foo') + assert 'foo' in options._arguments + + +def test_get_arguments(options): + options._arguments = ['foo'] + assert 'foo' in options.arguments + + +def test_raises_exception_if_argument_is_falsy(options): + with pytest.raises(ValueError): + options.add_argument(None) + + +def test_set_log_level(options): + options.log.level = 'debug' + assert options.log.level == 'debug' + + +def test_set_headless(options): + options.headless = True + assert '-headless' in options._arguments + + +def test_unset_headless(options): + options._arguments = ['-headless'] + options.headless = False + assert '-headless' not in options._arguments + + +def test_get_headless(options): + options._arguments = ['-headless'] + assert options.headless + + +def test_creates_capabilities(options): + profile = FirefoxProfile() + options._arguments = ['foo'] + options._binary = FirefoxBinary('/bar') + options._preferences = {'foo': 'bar'} + options._proxy = Proxy({'proxyType': ProxyType.MANUAL}) + options._profile = profile + options.log.level = 'debug' + caps = options.to_capabilities() + opts = caps.get(Options.KEY) + assert opts + assert 'foo' in opts['args'] + assert opts['binary'] == '/bar' + assert opts['prefs']['foo'] == 'bar' + assert opts['profile'] == profile.encoded + assert opts['proxy']['proxyType'] == ProxyType.MANUAL['string'] + assert opts['log']['level'] == 'debug' + + +def test_starts_with_default_capabilities(options): + from selenium.webdriver import DesiredCapabilities + assert options._caps == DesiredCapabilities.FIREFOX + + +def test_is_a_baseoptions(options): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(options, BaseOptions) diff --git a/test/unit/selenium/webdriver/ie/__init__.py b/test/unit/selenium/webdriver/ie/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/unit/selenium/webdriver/ie/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/unit/selenium/webdriver/ie/test_ie_options.py b/test/unit/selenium/webdriver/ie/test_ie_options.py new file mode 100644 index 00000000..dc2c7b6a --- /dev/null +++ b/test/unit/selenium/webdriver/ie/test_ie_options.py @@ -0,0 +1,184 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from selenium.webdriver.ie.options import Options, ElementScrollBehavior +import pytest + +TIMEOUT = 30 + + +@pytest.fixture +def opts(): + yield Options() + + +def test_arguments(opts): + arg1 = '-k' + arg2 = '-private' + opts.add_argument(arg1) + opts.add_argument(arg2) + assert arg1 in opts.arguments + assert arg2 in opts.arguments + + +def test_browser_attach_timeout(opts): + opts.browser_attach_timeout = TIMEOUT + assert opts.browser_attach_timeout == TIMEOUT + assert opts.options.get(Options.BROWSER_ATTACH_TIMEOUT) == TIMEOUT + + +def test_raises_exception_for_invalid_browser_attach_timeout(opts): + with pytest.raises(ValueError): + opts.browser_attach_timeout = 'foo' + + +def test_element_scroll_behavior(opts): + opts.element_scroll_behavior = ElementScrollBehavior.BOTTOM + assert opts.element_scroll_behavior == ElementScrollBehavior.BOTTOM + assert opts.options.get(Options.ELEMENT_SCROLL_BEHAVIOR) == ElementScrollBehavior.BOTTOM + + +def test_ensure_clean_session(opts): + opts.ensure_clean_session = True + assert opts.ensure_clean_session is True + assert opts.options.get(Options.ENSURE_CLEAN_SESSION) is True + + +def test_file_upload_dialog_timeout(opts): + opts.file_upload_dialog_timeout = TIMEOUT + assert opts.file_upload_dialog_timeout is TIMEOUT + assert opts.options.get(Options.FILE_UPLOAD_DIALOG_TIMEOUT) is TIMEOUT + + +def test_raises_exception_for_file_upload_dialog_timeout(opts): + with pytest.raises(ValueError): + opts.file_upload_dialog_timeout = 'foo' + + +def test_force_create_process_api(opts): + opts.force_create_process_api = True + assert opts.force_create_process_api is True + assert opts.options.get(Options.FORCE_CREATE_PROCESS_API) is True + + +def test_force_shell_windows_api(opts): + opts.force_shell_windows_api = True + assert opts.force_shell_windows_api is True + assert opts.options.get(Options.FORCE_SHELL_WINDOWS_API) is True + + +def test_full_page_screenshot(opts): + opts.full_page_screenshot = True + assert opts.full_page_screenshot is True + assert opts.options.get(Options.FULL_PAGE_SCREENSHOT) is True + + +def test_ignore_protected_mode_settings(opts): + opts.ignore_protected_mode_settings = True + assert opts.ignore_protected_mode_settings is True + assert opts.options.get(Options.IGNORE_PROTECTED_MODE_SETTINGS) is True + + +def test_ignore_zoom_level(opts): + opts.ignore_zoom_level = True + assert opts.ignore_zoom_level is True + assert opts.options.get(Options.IGNORE_ZOOM_LEVEL) is True + + +def test_initial_browser_url(opts): + url = 'http://www.seleniumhq.org' + opts.initial_browser_url = url + assert opts.initial_browser_url == url + assert opts.options.get(Options.INITIAL_BROWSER_URL) == url + + +def test_native_events(opts): + opts.native_events = True + assert opts.native_events is True + assert opts.options.get(Options.NATIVE_EVENTS) is True + + +def test_persistent_hover(opts): + opts.persistent_hover = True + assert opts.persistent_hover is True + assert opts.options.get(Options.PERSISTENT_HOVER) is True + + +def test_require_window_focus(opts): + opts.require_window_focus = True + assert opts.require_window_focus is True + assert opts.options.get(Options.REQUIRE_WINDOW_FOCUS) is True + + +def test_use_per_process_proxy(opts): + opts.use_per_process_proxy = True + assert opts.use_per_process_proxy is True + assert opts.options.get(Options.USE_PER_PROCESS_PROXY) is True + + +def test_validate_cookie_document_type(opts): + opts.validate_cookie_document_type = True + assert opts.validate_cookie_document_type is True + assert opts.options.get(Options.VALIDATE_COOKIE_DOCUMENT_TYPE) is True + + +def test_additional_options(opts): + opts.add_additional_option('foo', 'bar') + assert opts.additional_options.get('foo') == 'bar' + + +def test_to_capabilities(opts): + opts._options['foo'] = 'bar' + assert Options.KEY in opts.to_capabilities() + assert opts.to_capabilities().get(Options.KEY) == opts._options + + +def test_to_capabilities_arguments(opts): + arg = '-k' + opts.add_argument(arg) + caps_opts = opts.to_capabilities().get(Options.KEY) + assert caps_opts.get(Options.SWITCHES) == arg + + +def test_to_capabilities_additional_options(opts): + name = 'foo' + value = 'bar' + opts.add_additional_option(name, value) + caps_opts = opts.to_capabilities().get(Options.KEY) + assert caps_opts.get(name) == value + + +def test_to_capabilities_should_not_modify_set_options(opts): + opts._options['foo'] = 'bar' + arg = '-k' + opts.add_argument(arg) + opts.add_additional_option('baz', 'qux') + opts.to_capabilities().get(Options.KEY) + assert opts.options.get('foo') == 'bar' + assert opts.arguments[0] == arg + assert opts.additional_options.get('baz') == 'qux' + + +def test_starts_with_default_capabilities(opts): + from selenium.webdriver import DesiredCapabilities + assert opts._caps == DesiredCapabilities.INTERNETEXPLORER + + +def test_is_a_baseoptions(opts): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(opts, BaseOptions) diff --git a/test/unit/selenium/webdriver/opera/opera_options_tests.py b/test/unit/selenium/webdriver/opera/opera_options_tests.py new file mode 100644 index 00000000..523b8600 --- /dev/null +++ b/test/unit/selenium/webdriver/opera/opera_options_tests.py @@ -0,0 +1,87 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.opera.options import Options + + +@pytest.fixture +def options(): + return Options() + + +def test_set_android_package_name(options): + options.android_package_name = 'bar' + assert options._android_package_name == 'bar' + + +def test_get_android_package_name(options): + options._android_package_name = 'bar' + assert options.android_package_name == 'bar' + + +def test_set_android_device_socket(options): + options.android_device_socket = 'bar' + assert options._android_device_socket == 'bar' + + +def test_get_android_device_socket(options): + options._android_device_socket = 'bar' + assert options.android_device_socket == 'bar' + + +def test_set_android_command_line_file(options): + options.android_command_line_file = 'bar' + assert options._android_command_line_file == 'bar' + + +def test_get_android_command_line_file(options): + options._android_command_line_file = 'bar' + assert options.android_command_line_file == 'bar' + + +def test_creates_capabilities(options): + options._arguments = ['foo'] + options._binary_location = '/bar' + options._extensions = ['baz'] + options._debugger_address = '/foo/bar' + options._experimental_options = {'foo': 'bar'} + options._android_package_name = 'bar' + options._android_command_line_file = 'foo' + options._android_device_socket = 'spam' + caps = options.to_capabilities() + opts = caps.get(Options.KEY) + assert opts + assert 'foo' in opts['args'] + assert opts['binary'] == '/bar' + assert 'baz' in opts['extensions'] + assert opts['debuggerAddress'] == '/foo/bar' + assert opts['foo'] == 'bar' + assert opts['androidPackage'] == 'bar' + assert opts['androidCommandLineFile'] == 'foo' + assert opts['androidDeviceSocket'] == 'spam' + + +def test_starts_with_default_capabilities(options): + from selenium.webdriver import DesiredCapabilities + assert options._caps == DesiredCapabilities.OPERA + + +def test_is_a_baseoptions(options): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(options, BaseOptions) diff --git a/test/unit/selenium/webdriver/remote/__init__.py b/test/unit/selenium/webdriver/remote/__init__.py new file mode 100644 index 00000000..a5b1e6f8 --- /dev/null +++ b/test/unit/selenium/webdriver/remote/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/test/unit/selenium/webdriver/remote/test_error_handler.py b/test/unit/selenium/webdriver/remote/test_error_handler.py new file mode 100644 index 00000000..8c851caa --- /dev/null +++ b/test/unit/selenium/webdriver/remote/test_error_handler.py @@ -0,0 +1,255 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.common import exceptions +from selenium.webdriver.remote.errorhandler import ErrorCode, ErrorHandler + + +@pytest.fixture +def handler(): + yield ErrorHandler() + + +def test_does_not_raise_exception_on_success(handler): + assert handler.check_response({'status': ErrorCode.SUCCESS}) is None + assert handler.check_response({}) is None + + +@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_ELEMENT) +def test_raises_exception_for_no_such_element(handler, code): + with pytest.raises(exceptions.NoSuchElementException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_FRAME) +def test_raises_exception_for_no_such_frame(handler, code): + with pytest.raises(exceptions.NoSuchFrameException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_COMMAND) +def test_raises_exception_for_unknown_command(handler, code): + with pytest.raises(exceptions.WebDriverException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.STALE_ELEMENT_REFERENCE) +def test_raises_exception_for_stale_element_reference(handler, code): + with pytest.raises(exceptions.StaleElementReferenceException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.ELEMENT_NOT_VISIBLE) +def test_raises_exception_for_element_not_visible(handler, code): + with pytest.raises(exceptions.ElementNotVisibleException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_ELEMENT_STATE) +def test_raises_exception_for_invalid_element_state(handler, code): + with pytest.raises(exceptions.InvalidElementStateException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_ERROR) +def test_raises_exception_for_unknown_error(handler, code): + with pytest.raises(exceptions.WebDriverException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.ELEMENT_IS_NOT_SELECTABLE) +def test_raises_exception_for_element_not_selectable(handler, code): + with pytest.raises(exceptions.ElementNotSelectableException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.JAVASCRIPT_ERROR) +def test_raises_exception_for_javascript_error(handler, code): + with pytest.raises(exceptions.JavascriptException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.XPATH_LOOKUP_ERROR) +def test_raises_exception_for_xpath_lookup_error(handler, code): + with pytest.raises(exceptions.WebDriverException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.TIMEOUT) +def test_raises_exception_for_timeout(handler, code): + with pytest.raises(exceptions.TimeoutException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_WINDOW) +def test_raises_exception_for_no_such_window(handler, code): + with pytest.raises(exceptions.NoSuchWindowException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_COOKIE_DOMAIN) +def test_raises_exception_for_invalid_cookie_domain(handler, code): + with pytest.raises(exceptions.InvalidCookieDomainException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNABLE_TO_SET_COOKIE) +def test_raises_exception_for_unable_to_set_cookie(handler, code): + with pytest.raises(exceptions.UnableToSetCookieException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNEXPECTED_ALERT_OPEN) +def test_raises_exception_for_unexpected_alert_open(handler, code): + with pytest.raises(exceptions.UnexpectedAlertPresentException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.NO_ALERT_OPEN) +def test_raises_exception_for_no_alert_open(handler, code): + with pytest.raises(exceptions.NoAlertPresentException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.SCRIPT_TIMEOUT) +def test_raises_exception_for_script_timeout(handler, code): + with pytest.raises(exceptions.TimeoutException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_ELEMENT_COORDINATES) +def test_raises_exception_for_invalid_element_coordinates(handler, code): + with pytest.raises(exceptions.WebDriverException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.IME_NOT_AVAILABLE) +def test_raises_exception_for_ime_not_available(handler, code): + with pytest.raises(exceptions.ImeNotAvailableException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.IME_ENGINE_ACTIVATION_FAILED) +def test_raises_exception_for_ime_activation_failed(handler, code): + with pytest.raises(exceptions.ImeActivationFailedException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_SELECTOR) +def test_raises_exception_for_invalid_selector(handler, code): + with pytest.raises(exceptions.InvalidSelectorException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.SESSION_NOT_CREATED) +def test_raises_exception_for_session_not_created(handler, code): + with pytest.raises(exceptions.SessionNotCreatedException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS) +def test_raises_exception_for_move_target_out_of_bounds(handler, code): + with pytest.raises(exceptions.MoveTargetOutOfBoundsException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_XPATH_SELECTOR) +def test_raises_exception_for_invalid_xpath_selector(handler, code): + with pytest.raises(exceptions.InvalidSelectorException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER) +def test_raises_exception_for_invalid_xpath_selector_return_typer(handler, code): + with pytest.raises(exceptions.InvalidSelectorException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.ELEMENT_NOT_INTERACTABLE) +def test_raises_exception_for_element_not_interactable(handler, code): + with pytest.raises(exceptions.ElementNotInteractableException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INSECURE_CERTIFICATE) +def test_raises_exception_for_insecure_certificate(handler, code): + with pytest.raises(exceptions.InsecureCertificateException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_ARGUMENT) +def test_raises_exception_for_invalid_argument(handler, code): + with pytest.raises(exceptions.InvalidArgumentException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_COORDINATES) +def test_raises_exception_for_invalid_coordinates(handler, code): + with pytest.raises(exceptions.InvalidCoordinatesException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.INVALID_SESSION_ID) +def test_raises_exception_for_invalid_session_id(handler, code): + with pytest.raises(exceptions.InvalidSessionIdException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.NO_SUCH_COOKIE) +def test_raises_exception_for_no_such_cookie(handler, code): + with pytest.raises(exceptions.NoSuchCookieException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNABLE_TO_CAPTURE_SCREEN) +def test_raises_exception_for_unable_to_capture_screen_exception(handler, code): + with pytest.raises(exceptions.ScreenshotException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.ELEMENT_CLICK_INTERCEPTED) +def test_raises_exception_for_element_click_intercepted(handler, code): + with pytest.raises(exceptions.ElementClickInterceptedException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.UNKNOWN_METHOD) +def test_raises_exception_for_unknown_method(handler, code): + with pytest.raises(exceptions.UnknownMethodException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('code', ErrorCode.METHOD_NOT_ALLOWED) +def test_raises_exception_for_method_not_allowed(handler, code): + with pytest.raises(exceptions.WebDriverException): + handler.check_response({'status': code, 'value': 'foo'}) + + +@pytest.mark.parametrize('key', ['stackTrace', 'stacktrace']) +def test_relays_exception_stacktrace(handler, key): + import json + stacktrace = {'lineNumber': 100, 'fileName': 'egg', 'methodName': 'ham', 'className': 'Spam'} + value = {key: [stacktrace], + 'message': 'very bad', + 'error': ErrorCode.UNKNOWN_METHOD[0]} + response = {'status': 400, 'value': json.dumps({'value': value})} + with pytest.raises(exceptions.UnknownMethodException) as e: + handler.check_response(response) + + assert 'Spam.ham' in e.value.stacktrace[0] diff --git a/test/unit/selenium/webdriver/remote/test_new_session.py b/test/unit/selenium/webdriver/remote/test_new_session.py new file mode 100644 index 00000000..c5bf6b0c --- /dev/null +++ b/test/unit/selenium/webdriver/remote/test_new_session.py @@ -0,0 +1,72 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from copy import deepcopy +from importlib import import_module + +import pytest + +from selenium.webdriver import DesiredCapabilities +from selenium.webdriver.remote.command import Command +from selenium.webdriver.remote.webdriver import WebDriver + + +def test_converts_oss_capabilities_to_w3c(mocker): + mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute') + oss_caps = {'platform': 'WINDOWS', 'version': '11', 'acceptSslCerts': True} + w3c_caps = {'platformName': 'windows', 'browserVersion': '11', 'acceptInsecureCerts': True} + WebDriver(desired_capabilities=deepcopy(oss_caps)) + expected_params = {'capabilities': {'firstMatch': [{}], 'alwaysMatch': w3c_caps}, + 'desiredCapabilities': oss_caps} + mock.assert_called_with(Command.NEW_SESSION, expected_params) + + +def test_converts_proxy_type_value_to_lowercase_for_w3c(mocker): + mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute') + oss_caps = {'proxy': {'proxyType': 'MANUAL', 'httpProxy': 'foo'}} + w3c_caps = {'proxy': {'proxyType': 'manual', 'httpProxy': 'foo'}} + WebDriver(desired_capabilities=deepcopy(oss_caps)) + expected_params = {'capabilities': {'firstMatch': [{}], 'alwaysMatch': w3c_caps}, + 'desiredCapabilities': oss_caps} + mock.assert_called_with(Command.NEW_SESSION, expected_params) + + +def test_works_as_context_manager(mocker): + mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.execute') + quit_ = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.quit') + + with WebDriver() as driver: + assert isinstance(driver, WebDriver) + + assert quit_.call_count == 1 + + +@pytest.mark.parametrize('browser_name', ['firefox', 'chrome', 'ie', 'opera']) +def test_accepts_firefox_options_to_remote_driver(mocker, browser_name): + options = import_module('selenium.webdriver.{}.options'.format(browser_name)) + caps_name = browser_name.upper() if browser_name != 'ie' else 'INTERNETEXPLORER' + mock = mocker.patch('selenium.webdriver.remote.webdriver.WebDriver.start_session') + + opts = options.Options() + opts.add_argument('foo') + expected_caps = getattr(DesiredCapabilities, caps_name) + caps = expected_caps.copy() + expected_caps.update(opts.to_capabilities()) + + WebDriver(desired_capabilities=caps, options=opts) + mock.assert_called_with(expected_caps, None) diff --git a/test/unit/selenium/webdriver/remote/test_remote_connection.py b/test/unit/selenium/webdriver/remote/test_remote_connection.py new file mode 100644 index 00000000..4441982f --- /dev/null +++ b/test/unit/selenium/webdriver/remote/test_remote_connection.py @@ -0,0 +1,64 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +try: + from urllib import parse +except ImportError: # above is available in py3+, below is py2.7 + import urlparse as parse + +from selenium import __version__ +from selenium.webdriver.remote.remote_connection import ( + RemoteConnection, +) + + +def test_get_remote_connection_headers_defaults(): + url = 'http://remote' + headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url)) + assert 'Authorization' not in headers.keys() + assert 'Connection' not in headers.keys() + assert headers.get('Accept') == 'application/json' + assert headers.get('Content-Type') == 'application/json;charset=UTF-8' + assert headers.get('User-Agent').startswith("selenium/%s (python " % __version__) + assert headers.get('User-Agent').split(' ')[-1] in {'windows)', 'mac)', 'linux)'} + + +def test_get_remote_connection_headers_adds_auth_header_if_pass(): + url = 'http://user:pass@remote' + headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url)) + assert headers.get('Authorization') == 'Basic dXNlcjpwYXNz' + + +def test_get_remote_connection_headers_adds_keep_alive_if_requested(): + url = 'http://remote' + headers = RemoteConnection.get_remote_connection_headers(parse.urlparse(url), keep_alive=True) + assert headers.get('Connection') == 'keep-alive' + + +class MockResponse: + code = 200 + headers = [] + + def read(self): + return b"{}" + + def close(self): + pass + + def getheader(self, *args, **kwargs): + pass diff --git a/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py b/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py new file mode 100644 index 00000000..508bd90f --- /dev/null +++ b/test/unit/selenium/webdriver/webkitgtk/webkitgtk_options_tests.py @@ -0,0 +1,67 @@ +# Licensed to the Software Freedom Conservancy (SFC) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The SFC licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest + +from selenium.webdriver.webkitgtk.options import Options + + +@pytest.fixture +def options(): + return Options() + + +def test_set_binary_location(options): + options.binary_location = '/foo/bar' + assert options._binary_location == '/foo/bar' + + +def test_get_binary_location(options): + options._binary_location = '/foo/bar' + assert options.binary_location == '/foo/bar' + + +def test_set_overlay_scrollbars_enabled(options): + options.overlay_scrollbars_enabled = False + assert options._overlay_scrollbars_enabled is False + + +def test_get_overlay_scrollbars_enabled(options): + options._overlay_scrollbars_enabled = True + assert options.overlay_scrollbars_enabled is True + + +def test_creates_capabilities(options): + options._arguments = ['foo'] + options._binary_location = '/bar' + options._overlay_scrollbars_enabled = True + caps = options.to_capabilities() + opts = caps.get(Options.KEY) + assert opts + assert 'foo' in opts['args'] + assert opts['binary'] == '/bar' + assert opts['useOverlayScrollbars'] is True + + +def test_starts_with_default_capabilities(options): + from selenium.webdriver import DesiredCapabilities + assert options._caps == DesiredCapabilities.WEBKITGTK + + +def test_is_a_baseoptions(options): + from selenium.webdriver.common.options import BaseOptions + assert isinstance(options, BaseOptions) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..3d9b4c48 --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +[tox] +envlist = py{27,34,35,36}-{unit,chrome,edge,firefox,ie,remote,safari}, docs, flake8 + +[testenv] +passenv = DISPLAY PYTEST_ADDOPTS CI TRAVIS +commands = + py{27,34,35,36}-unit: py.test -n=auto {posargs:test/unit} + py{27,36}-chrome: py.test --driver=Chrome {posargs} + py{27,36}-edge: py.test --driver=Edge {posargs} + py{27,36}-firefox: py.test --driver=Firefox {posargs} + py{27,36}-ie: py.test --driver=Ie {posargs} + py{27,36}-marionette: py.test --driver=Marionette {posargs} + py{27,36}-remote: py.test --driver=Remote {posargs} + py{27,36}-safari: py.test --driver=Safari {posargs} +install_command = pip install -v --no-index --find-links=../third_party/py {opts} {packages} +deps = + pytest==3.0.3 + pytest-instafail==0.3.0 + pytest-mock==1.5.0 + py{27,33,34,35,36}-{unit,chrome,firefox,marionette}: pytest-xdist==1.15 + urllib3==1.23 + +[testenv:docs] +skip_install = true +deps = + Sphinx==1.8.2 +commands = sphinx-build -W -b html -d ../build/doctrees docs/source ../build/docs/api/py {posargs} + +[testenv:flake8] +skip_install = true +deps = flake8 +commands = flake8 {posargs}