diff --git a/docs/extending/event_firing_webdriver/MyListener.py b/docs/extending/event_firing_webdriver/MyListener.py new file mode 100644 index 000000000..cb550e276 --- /dev/null +++ b/docs/extending/event_firing_webdriver/MyListener.py @@ -0,0 +1,23 @@ +from robot.api import logger +from selenium.webdriver.support.events import AbstractEventListener + + +class MyListener(AbstractEventListener): + + def before_navigate_to(self, url, driver): + logger.info("Before navigate to %s" % url) + + def after_navigate_to(self, url, driver): + logger.info("After navigate to %s" % url) + + def before_click(self, element, driver): + logger.info("Before click") + + def after_click(self, element, driver): + logger.info("After click") + + def before_change_value_of(self, element, driver): + logger.info("Before clear and send_keys") + + def after_change_value_of(self, element, driver): + logger.info("After clear and send_keys") diff --git a/docs/extending/event_firing_webdriver/event_firing_webdriver.robot b/docs/extending/event_firing_webdriver/event_firing_webdriver.robot new file mode 100644 index 000000000..1b7112976 --- /dev/null +++ b/docs/extending/event_firing_webdriver/event_firing_webdriver.robot @@ -0,0 +1,21 @@ +*** Settings *** +Library SeleniumLibrary event_firing_webdriver=${CURDIR}/MyListener.py +Suite Teardown Close All Browsers + +*** Variables *** +${URL} https://github.com/robotframework/SeleniumLibrary +${ISSUES} ${URL}/issues +${BROWSER} Chrome + +*** Test Cases *** +Open Browser To Start Page + Open Browser ${URL} ${BROWSER} + +Event Firing Webdriver Go To (WebDriver) + Go To ${ISSUES} + +Event Firing Webdriver Click Element (WebElement) + Click Element js-issues-search + +Event Firing Webdriver Input Text (WebElement) + Input Text js-issues-search FooBar diff --git a/docs/extending/event_firing_webdriver/readme.rst b/docs/extending/event_firing_webdriver/readme.rst new file mode 100644 index 000000000..8356c9581 --- /dev/null +++ b/docs/extending/event_firing_webdriver/readme.rst @@ -0,0 +1,18 @@ +EventFiringWebDriver +==================== + +This example demonstrates the EventFiringWenDriver support. The MyListener.py +does only log before and after the Selenium API call. But MyListener.py +could make new Selenium API calls or anything else which is possible from the Python. + +To run the example, give command:: + + robot event_firing_webdriver.robot + +From the generated log.html, in the keywords logging, look at logging. +The lines starting "Before " and "After " are from the EventFiringWenDriver. + +See `Robot Framework test suite`_ and `EventFiringWebDriver class`_ for further details. + +.. _Robot Framework test suite: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/event_firing_webdriver/event_firing_webdriver.robot +.. _EventFiringWebDriver class: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/event_firing_webdriver/MyListener.py diff --git a/docs/extending/extending/decomposition/Decomposition.py b/docs/extending/extending/decomposition/Decomposition.py new file mode 100644 index 000000000..6be35969a --- /dev/null +++ b/docs/extending/extending/decomposition/Decomposition.py @@ -0,0 +1,38 @@ +from SeleniumLibrary import SeleniumLibrary +from SeleniumLibrary.base import keyword, LibraryComponent +from SeleniumLibrary.keywords import BrowserManagementKeywords + + +class BrowserKeywords(LibraryComponent): + + def __init__(self, ctx): + LibraryComponent.__init__(self, ctx) + + @keyword + def open_browser(self, host): + url = 'http://{}.com/'.format(host) + browser_management = BrowserManagementKeywords(self.ctx) + browser_management.open_browser(url, 'chrome') + + +class DesiredCapabilitiesKeywords(LibraryComponent): + + def __init__(self, ctx): + LibraryComponent.__init__(self, ctx) + + @keyword + def get_browser_desired_capabilities(self): + self.info('Getting currently open browser desired capabilities') + return self.driver.desired_capabilities + + +class Decomposition(SeleniumLibrary): + + def __init__(self, timeout=5.0, implicit_wait=0.0, + run_on_failure='Capture Page Screenshot', + screenshot_root_directory=None): + SeleniumLibrary.__init__(self, timeout=timeout, implicit_wait=implicit_wait, + run_on_failure=run_on_failure, + screenshot_root_directory=screenshot_root_directory) + self.add_library_components([BrowserKeywords(self), + DesiredCapabilitiesKeywords(self)]) diff --git a/docs/extending/extending/decomposition/decomposition.robot b/docs/extending/extending/decomposition/decomposition.robot new file mode 100644 index 000000000..e62c78e46 --- /dev/null +++ b/docs/extending/extending/decomposition/decomposition.robot @@ -0,0 +1,9 @@ +*** Settings *** +Library ./Decomposition.py + +*** Test Cases *** +Decomposition Example + Open Browser google + ${capabilities} = Get Browser Desired Capabilities + Log ${capabilities} + [Teardown] Close Browser diff --git a/docs/extending/extending/get_instance/GetSeleniumLibraryInstance.py b/docs/extending/extending/get_instance/GetSeleniumLibraryInstance.py new file mode 100644 index 000000000..c734a12ab --- /dev/null +++ b/docs/extending/extending/get_instance/GetSeleniumLibraryInstance.py @@ -0,0 +1,14 @@ +from robot.api import logger +from robot.libraries.BuiltIn import BuiltIn + + +def open_browser(host): + url = 'http://{}.com/'.format(host) + sl = BuiltIn().get_library_instance('SeleniumLibrary') + sl.open_browser(url, 'chrome') + + +def get_browser_desired_capabilities(): + logger.info('Getting currently open browser desired capabilities') + sl = BuiltIn().get_library_instance('SeleniumLibrary') + return sl.driver.desired_capabilities diff --git a/docs/extending/extending/get_instance/instance.robot b/docs/extending/extending/get_instance/instance.robot new file mode 100644 index 000000000..716d6bbcd --- /dev/null +++ b/docs/extending/extending/get_instance/instance.robot @@ -0,0 +1,10 @@ +*** Settings *** +Library SeleniumLibrary +Library ./GetSeleniumLibraryInstance.py + +*** Test Cases *** +Use InheritSeleniumLibrary Open Browser Keyword + GetSeleniumLibraryInstance.Open Browser google + ${capabilities} = GetSeleniumLibraryInstance.Get Browser Desired Capabilities + Log ${capabilities} + [Teardown] Close Browser \ No newline at end of file diff --git a/docs/extending/extending/inheritance/InheritSeleniumLibrary.py b/docs/extending/extending/inheritance/InheritSeleniumLibrary.py new file mode 100644 index 000000000..634b4ce79 --- /dev/null +++ b/docs/extending/extending/inheritance/InheritSeleniumLibrary.py @@ -0,0 +1,26 @@ +from robot.api import logger +from SeleniumLibrary import SeleniumLibrary +from SeleniumLibrary.base import keyword +from SeleniumLibrary.keywords import BrowserManagementKeywords + + +class InheritSeleniumLibrary(SeleniumLibrary): + + @keyword + def open_browser(self, host): + url = 'http://{}.com/'.format(host) + browser_management = BrowserManagementKeywords(self) + browser_management.open_browser(url, 'chrome') + + @keyword + def get_browser_desired_capabilities(self): + logger.info('Getting currently open browser desired capabilities') + return self.driver.desired_capabilities + + def not_keywords_but_public_methods(self): + logger.info('Python public method not a keyword, because it is not ' + 'decorated with @keyword decorator') + + def _private_method_are_not_keywords(self): + logger.info('Python private method is not a keyword, because it is not ' + 'decorated with @keyword decorator') diff --git a/docs/extending/extending/inheritance/inheritance.robot b/docs/extending/extending/inheritance/inheritance.robot new file mode 100644 index 000000000..83a6522dd --- /dev/null +++ b/docs/extending/extending/inheritance/inheritance.robot @@ -0,0 +1,9 @@ +*** Settings *** +Library ./InheritSeleniumLibrary.py + +*** Test Cases *** +Use InheritSeleniumLibrary Open Browser Keyword + Open Browser google + ${capabilities} = Get Browser Desired Capabilities + Log ${capabilities} + [Teardown] Close Browser \ No newline at end of file diff --git a/docs/extending/extending_seleniumlibrary.rst b/docs/extending/extending_seleniumlibrary.rst new file mode 100644 index 000000000..e51061d93 --- /dev/null +++ b/docs/extending/extending_seleniumlibrary.rst @@ -0,0 +1,366 @@ +Extending SeleniumLibrary +========================= + +.. contents:: + +Introduction +============ +SeleniumLibrary offers three main ways to creating new functionality for SeleniumLibrary: Plugin +API, `EventFiringWebDriver`_ and building new libraries on top the SeleniumLibrary (later +referred as extending SeleniumLibrary.) Plugin API and extending SeleniumLibrary allows +similar access to the SeleniumLibrary public API and offers their own pros and cons for +building custom functionality on top the SeleniumLibrary. The EventFiringWebDriver offers +lister like interface to the Selenium API. The plugin API and EventFiringWebDriver +are new in SeleniumLibrary 4.0. + +Plugin API +---------- +SeleniumLibrary offers plugins as a way to modify, add library keywords and modify some of the internal +functionality without creating new library or hacking the source code. Plugins can be only loaded in the +library import, with the `plugins`_ argument and SeleniumLibrary does not offer way to unload the +plugins from the SeleniumLibrary. Creating new plugins is more strict than creating new libraries, but +plugins allows more access to the methods that are used to implement the keywords. + +EventFiringWebDriver +-------------------- +The EventFiringWebDriver is an listener type of API offered by the Selenium. The EventFiringWebDriver +allows to listen Selenium API calls and allows users to fire events before and after Selenium API methods. +Refer the to the Selenium `EventFiringWebDriver`_ documentation what Selenium API methods are +supported and how to EventFiringWebDriver works. + +Extending SeleniumLibrary +------------------------- +As with any Robot Framework Python library, new libraries can be build on top of the SeleniumLibrary +by inheriting the SeleniumLibrary, getting active library instance from Robot Framework or by other +means which are available from Python or from Robot Framework. Building new libraries allows +library creator freedom to choose which keywords new library offers and more flexibility how +new libraries are created than what the plugin API offers. + +Contributing to SeleniumLibrary project +--------------------------------------- +Before making your own plugin or library on top of the SeleniumLibrary, private or public, please consider +would the plugin or extension be generally useful in the SeleniumLibrary. If the plugin or extension would +be useful for others, then please create an issue and perhaps a pull request. More details how +SeleniumLibrary project handles the enhancement requests can be read from the CONTRIBUTING.rst +`Enhancement requests`_ chapter. + +Public API +========== +The plugin API and extending SeleniumLibrary have same access to the SeleniumLibrary public API. +All the methods, which are exposed as keywords, are available in the SeleniumLibrary public API. +Generally keywords are converted to lover case and spaces are converted to underscores. Example +`Open Browser`_ keyword is available as ``open_browser`` method. The method name can be +overwritten with the ``@keyword`` decorator, but the SeleniumLibrary 4.0.0 release does not +contain keywords where the keyword name would differ from the method name (other than the keyword +case.) Please note that keywords created by the plugins may not follow these rules and it is good +to verify the method name from the plugin API source. + +Methods and attributes which are not keywords but are available in the public API +--------------------------------------------------------------------------------- +The SeleniumLibrary also contains methods and attributes which are not keywords, but are +useful when creating plugin or extending the SeleniumLibrary. The available methods are: + +================ ================================================================================ + Method Description +================ ================================================================================ +find_element Finds first element matching ``locator``. +find_elements Find all elements matching ``locator``. +get_keyword_tags Responsible for returning keywords tags for Robot Framework dynamic library API. +register_driver Add's a Selenium ``driver`` to the library WebDriverCache. +run_keyword Responsible for executing keywords by Robot Framework dynamic library API. +failure_occurred Method that is executed when a SeleniumLibrary keyword fails. +================ ================================================================================ + +Also there are the following public attributes available: + +========================= ================================================================ + Attribute Description +========================= ================================================================ +driver Current active driver. +event_firing_webdriver Reference to a class implementing event firing selenium support. +timeout Default value for ``timeouts`` used with ``Wait ...`` keywords. +implicit_wait Default value for ``implicit wait`` used when locating elements. +run_on_failure_keyword Default action for the `run-on-failure functionality`. +screenshot_root_directory Location where possible screenshots are created +========================= ================================================================ + +For more details about the methods, please read the individual method documentation and many +of the attributes are explained in the library `keyword documentation`_. please note that +plugins may alter the functionality of the method or attributes and documentation applies +only for the core SeleniumLibrary. + +Plugins +======= +SeleniumLibrary offers plugins as a way to modify, add library keywords and modify some of the internal +functionality without creating new library or hacking the source code. See `plugin example`_ how plugins +can be implemented. + +Importing plugins +----------------- +Importing plugins is similar when importing Robot Framework `libraries`_. It is possible import plugin +with using `physical path`_ or with `plugin name`_ exactly in same way as importing libraries in +Robot Framework. SeleniumLibrary plugins are searched from the same `module search path`_ as +Robot Framework searches libraries. It is only possible to import plugins written in Python, other +programming languages or Robot Framework test data is not supported. Like with Robot Framework +library imports, plugin names are case sensitive and spaces are not supported in the plugin name. +It is possible to import multiple plugins at the same time by separating plugins with comma. It +is possible to have space before and after the comma. Plugins are imported in the order they defined +in the `plugins`_ argument. If two or more plugins declare the same keyword or modify the same +method/attribute in the SeleniumLibrary, the last plugin to perform the changes will overwrite +the changes made by other plugins. Example of plugin imports:: + + | Library | SeleniumLibrary | plugins=${CURDIR}/MyPlugin.py | # Imports plugin with physical path | + | Library | SeleniumLibrary | plugins=plugins.MyPlugin, plugins.MyOtherPlugin | # Import two plugins with name | + + +Plugin arguments +---------------- +When SeleniumLibrary creates instances from the plugin classes, it will by default initiate the class +with a single argument, called ``ctx`` (context). ``ctx`` is the instance of the SeleniummLibrary and +it provides access to the SeleniumLibrary `Public API`_. + +It is also possible to provide optional arguments to the plugins. Arguments must be separated with a +semicolon from the plugin. SeleniumLibrary will not convert arguments to any specific type and everything +is by default unicode. Plugin is responsible for converting the argument to proper types. Example of +importing plugin with arguments:: + + | Library | SeleniumLibrary | plugins=plugins.Plugin;ArgOne;ArgTwo | # Import two plugins with two arguments: ArgOne and ArgTwo | + +It is also possible to provide variable number of arguments and keywords arguments. Named arguments +must be defined first, variable number of arguments as second and keywords arguments as last. +All arguments must be separated with semicolon. Example if plugin __init__ is defined like this:: + + class Plugin(LibraryComponent): + + def __init__(self, ctx, arg, *varargs, **kwargs): + # Code to implement the plugin. + +Then, for example, it is possible to plugin with these arguments:: + + | Library | SeleniumLibrary | plugins=plugins.Plugin;argument1;varg1;varg2;kw1=kwarg1;kw2=kwarg2 | + +Then the ``argument1`` is given the ``arg`` in the ``__init__``. The ``varg1`` and ``varg2`` variable +number arguments are given to the ``*varargs`` argument in the ``__init__``. Finally, the ``kw1=kwarg1`` +and ``kw2=kwarg2`` keyword arguments are given to the ``**kwargs`` in the ``__init__``. As in Python, +there can be zero or more variable number and keyword arguments. + +Plugin API +---------- +Generally speaking, plugins are not any different from the classes that are used to implement keyword +in the SeleniumLibrary. Example like with `BrowserManagementKeywords`_ class inherits the `LibraryComponent`_ +and uses ``@keyword`` decorator to mark which methods are exposed as keywords. + +Plugins must be implemented as Python classes and plugins must inherit the SeleniumLibrary `LibraryComponent`_ +class. Plugin __init__ must support at least one argument: ``ctx``. Also optional arguments are supported, see +`Plugin arguments`_ for more details how to provide optional arguments to plugins. + +SeleniumLibrary uses Robot Framework `dynamic library API`_. The main difference, when compared to libraries +using dynamic library API, is that plugins are not responsible for implementing the dynamic library API. +SeleniumLibrary is handling the dynamic library API requirements towards Robot Framework. For plugins +this means that methods that implements keywords, must be decorated with ``@keyword`` decorator. The ``@keyword`` +decorator can be imported from Robot Framework and used in the following way:: + + from robot.api.deco import keyword + + class Plugin(LibraryComponent): + + @keyword + def keyword(self): + self.driver.... # More code here to implement the keyword + +Handling plugins failures +------------------------- +SeleniumLibrary does not suppress exception raised during plugin import or during keywords discovery from the +plugins. In this case the whole SeleniumLibrary import will fail and SeleniumLibrary keywords can not be used +from that import. + +By default when exceptions raised by SeleniumLibrary keywords will trigger the `run on failure`_ functionality, +this also applies keywords created or modified by the plugins. But it must be noted that plugins can alter the +SeleniumLibrary run on failure functionality and refer to the plugin documentation for further details. + +LibraryComponent +---------------- +Although ``ctx`` provides access to the SeleniumLibrary `Public API`_, the `LibraryComponent`_ provides more +methods and attributes and also an IDE friendly access to the plugin API, Example currently active +browser can be found from ``self.ctx.driver``, the ``LibraryComponent`` exposes the browser as: +``self.driver`` and most IDE can discover the completion automatically. Plugin classes must inherit +the ``LibraryComponent``. + +The following methods are available from the ``LibraryComponent`` class: + +======================== ================================================================================================================================================= + Method Description +======================== ================================================================================================================================================= +find_element Finds first element matching ``locator``. +find_elements Find all elements matching ``locator``. +is_text_present Returns True if text is present in the page. +is_element_enabled Returns True if element is enabled. +is_visible Returns True if element is visible. +log_source Calls method defining the `Log Source` keyword. +assert_page_contains Raises AssertionError if element is not found from the page. +assert_page_not_contains Raises AssertionError if element is found from the page. +get_timeout By default returns SeleniumLibrary ``timeout`` argument value. With argument converts string with Robot Framework ``timestr_to_secs`` to seconds. +info Wrapper to ``robot.api.logger.info`` method. +debug Wrapper to ``robot.api.logger.debug`` method. +warn Wrapper to ``robot.api.logger.warn`` method. +log Wrapper to ``robot.api.logger.write`` method. +======================== ================================================================================================================================================= + +Also following attributes are available from the ``LibraryComponent`` class: + +============== ===================================================================== + Attribute Description +============== ===================================================================== +driver Currently active browser/WebDriver instance in the SeleniumLibrary. +drivers `Cache`_ for the opened browsers/WebDriver instances. +element_finder Read/write attribute for the `ElementFinder`_ instance. +ctx Instance of the SeleniumLibrary. +log_dir Folder where output files are written. +============== ===================================================================== + +See the `SeleniumLibrary init`_, the `LibraryComponent`_ and the `ContextAware`_ classes for further +implementation details. + +Generating keyword documentation +-------------------------------- +To separate keywords which are added or modified by plugins, SeleniumLibrary will add ``plugin`` `keyword tag`_ +to all keywords added or modified from plugins. When SeleniumLibrary keyword documentation, with plugins, +is generated by `libdoc`_ it is easy to separate keywords which are added or modified by plugins. Keyword +documentation can be example generated by following command:: + + python -m robot.libdoc SeleniumLibrary::plugins=/path/to/Plugin.py ./SeleniumLibraryWithPlugin.html + + +EventFiringWebDriver support +============================ +The `EventFiringWebDriver`_ is an listener type of API offered by the Selenium. In practice ``EventFiringWebDriver`` +offers way to intercept Selenium API call, made by SeleniumLibrary or by other library keywords and fire +separate Selenium events. Events can be fired before and after Selenium API call. + +SeleniumLibrary offers support for Selenium ``EventFiringWebDriver`` listener class by providing possibility +to import the listener class by `event_firing_webdriver`_ argument. Importing ``EventFiringWebDriver`` +is similar when importing Robot Framework `libraries`_. It is possible import ``EventFiringWebDriver`` +with using `physical path`_ or with `name`_ exactly in same way as importing libraries in +Robot Framework. ``EventFiringWebDriver`` class is searched from the same `module search path`_ as +Robot Framework searches libraries. It is only possible to import ``EventFiringWebDriver`` class +written in Python, other programming languages or Robot Framework test data is not supported. Like with +Robot Framework library imports, ``EventFiringWebDriver`` class name is case sensitive and spaces +are not supported in the class name. It is possible to import only one ``EventFiringWebDriver`` class. +Example of ``EventFiringWebDriver`` imports:: + + | Library | SeleniumLibrary | event_firing_webdriver=${CURDIR}/MyListener.py | # Imports EventFiringWebDriver with physical path | + +Refer the to the Selenium `EventFiringWebDriver`_ documentation what Selenium API methods are +supported and how to EventFiringWebDriver works. Also there is simple +`EventFiringWebDriver example`_ for more details. + +Extending SeleniumLibrary +========================= +Starting from SeleniumLibrary 3.0, the library has moved to use Robot Framework +`dynamic library API`_. To ease the usage of the dynamic library API, the SeleniumLibrary uses +a `PythonLibCore`_ project to handle the most the dynamic library API requirements, except running +the keyword and providing keywords tags. For more details please about the dynamic library API, +read the Robot Framework `dynamic library API`_ documentation. + + +General principles for extending SeleniumLibrary +------------------------------------------------ +The principles described in the Robot Framework User Guide, `Extending existing test libraries`_ +chapter also apply when extending the SeleniumLibrary. There are two different ways to +extend the SeleniumLibrary. + +1) Create a library which also the contains existing SeleniumLibrary keywords, example by using `inheritance`_. +2) Create library which contains only new keywords. + +When creating a library, which also includes the existing SeleniumLibrary keywords, there are +extra steps which needs to be taken account, because SeleniumLibrary uses `PythonLibCore`_ +and the `dynamic library API`_. All methods which should be published as keywords must be +decorated with ``@keyword`` decorator. The ``@keyword`` decorator can be imported in following way:: + + from robot.api.deco import keyword + +Keywords should be inside of a ``class`` and the ``add_library_components`` method +must be called to add keywords. The ``add_library_components`` method is inherited from the +`PythonLibCore`_ project and the method must contains list of classes which contains the +new keywords. + +Creating a new library by using inheritance +------------------------------------------- +Perhaps the easiest way to extend the SeleniumLibrary is to inherit the SeleniumLibrary and add +new keywords methods to a new library. The `inheritance example`_ shows how to declare new +keyword ``Get Browser Desired Capabilities`` and how to overwrite existing ``Open Browser`` keyword. + +Because the ``InheritSeleniumLibrary`` class foes not overwrite the SeleniumLibrary ``init`` method, +the ``add_library_components`` is called automatically. Then the ``InheritSeleniumLibrary`` class methods +which are decorated with ``@keyword`` decorator are added to the ``InheritSeleniumLibrary`` +library keywords. Also existing keywords from SeleniumLibrary are added as library keywords. + +Because the methods are not directly available in the SeleniumLibrary class, it not +possible to call the original method example like this:: + + super(ClassName, self).open_browser(url, browser, alias, remote_url, + desired_capabilities, ff_profile_dir) + +Instead user must call the method from the class instance which implements the keyword, example:: + + browser_management = BrowserManagementKeywords(self) + browser_management.open_browser(url, 'chrome') + +Creating a new library from multiple classes +-------------------------------------------- +Decomposition is a good way to split library to smaller name spaces and it usually eases the +library testing. The `decomposition example`_ shows how the ``Get Browser Desired Capabilities`` +and ``Open Browser`` keywords can divided to own classes. + +The example also shows the usage of the ``ctx`` (context) object and the `LibraryComponent`_ class. +The ``ctx`` object is a instance of the SeleniumLibrary which provides access to the +SeleniumLibrary `Public API`_ for the ``BrowserKeywords`` and ``DesiredCapabilitiesKeywords`` classes. + +The ``LibraryComponent`` is a wrapper class, which provides easier shortcuts the ``ctx`` object +methods and example provides general logging methods. Example the Selenium WebDriver instance in +the context: ``self.ctx.driver``, but the ``LibraryComponent`` provides a shortcut and it can be +accessed with: ``self.driver`` + + +Creating a new library by getting active library instance +--------------------------------------------------------- +Getting the active library instance provides way to create a new library that it does not +automatically contain keywords from the SeleniumLibrary. This eases the name space +handling and if only new keywords are created, user does not have to prefix the keywords with the +library name. This way also allows user to freely choose the Robot Framework `library API`_. +The `instance example`_ shows a way how the active SeleniumLibrary is get from the Robot Framework. +The example shows how to declare ``Get Browser Desired Capabilities`` and ``Open Browser`` keywords +to the new library and the `instance example`_ uses the `static keyword API`_ to declare new +keywords. + +.. _EventFiringWebDriver: https://seleniumhq.github.io/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.event_firing_webdriver.html#module-selenium.webdriver.support.event_firing_webdriver +.. _plugins: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Importing +.. _Enhancement requests: https://github.com/robotframework/SeleniumLibrary/blob/master/CONTRIBUTING.rst#enhancement-requests +.. _dynamic library API: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#dynamic-library-api +.. _PythonLibCore: https://github.com/robotframework/PythonLibCore +.. _Open Browser: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Open%20Browser +.. _keyword documentation: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html +.. _libraries: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#importing-libraries +.. _plugin example: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/plugin_api/readme.rst +.. _physical path: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-physical-path-to-library +.. _plugin name: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-library-name +.. _module search path: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#module-search-path +.. _BrowserManagementKeywords: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/keywords/browsermanagement.py +.. _run on failure: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Run-on-failure%20functionality +.. _Cache: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/keywords/webdrivertools.py +.. _ElementFinder: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/locators/elementfinder.py +.. _SeleniumLibrary init: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/__init__.py +.. _ContextAware: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/context.py +.. _keyword tag: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#keyword-tags +.. _libdoc: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#library-documentation-tool-libdoc +.. _event_firing_webdriver: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Importing +.. _EventFiringWebDriver example: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/event_firing_webdriver/readme.rst +.. _Extending existing test libraries: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#extending-existing-test-libraries +.. _name: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-library-name +.. _inheritance: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending/inheritance/InheritSeleniumLibrary.py +.. _inheritance example: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending/inheritance/InheritSeleniumLibrary.py +.. _decomposition example: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending/decomposition/Decomposition.py +.. _instance example: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending/get_instance/GetSeleniumLibraryInstance.py +.. _LibraryComponent: https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/librarycomponent.py +.. _library API: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#different-test-library-apis +.. _static keyword API: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#creating-static-keywords diff --git a/docs/extending/plugin_api/MyPlugin.py b/docs/extending/plugin_api/MyPlugin.py new file mode 100644 index 000000000..5ebfe4b1f --- /dev/null +++ b/docs/extending/plugin_api/MyPlugin.py @@ -0,0 +1,36 @@ +from robot.api import logger + +from SeleniumLibrary.base import LibraryComponent, keyword +from SeleniumLibrary.locators import ElementFinder + + +class DummyFinder(object): + + def __init__(self, ctx): + self.ctx = ctx + + def find(self, *args): + logger.info('DummyFinder args "%s"' % str(args)) + logger.info('Original finder %s' + % self.ctx._original_element_finder ) + return 'Dummy find' + + +class MyPlugin(LibraryComponent): + + def __init__(self, ctx): + LibraryComponent.__init__(self, ctx) + ctx._original_element_finder = ElementFinder(ctx) + self.element_finder = DummyFinder(ctx) + + @keyword + def new_keyword(self): + """Adding new keyword.""" + self.info('New Keyword') + return 'New Keyword' + + @keyword() + def open_browser(self, location): + """Overwrite existing keyword.""" + self.info(location) + return location diff --git a/docs/extending/plugin_api/adding_plugin.robot b/docs/extending/plugin_api/adding_plugin.robot new file mode 100644 index 000000000..f1718111f --- /dev/null +++ b/docs/extending/plugin_api/adding_plugin.robot @@ -0,0 +1,15 @@ +*** Settings *** +Library SeleniumLibrary plugins=${CURDIR}/MyPlugin.py + +*** Test Cases *** +Adding New Keyword From Class + ${text} = New Keyword + Should Be Equal ${text} New Keyword + +Overwriting Exsisting Keyword + ${text} = Open Browser text is returned + Should Be Equal ${text} text is returned + +Oerwriting ElementFinder + ${element} = Get WebElement //div + Should Be Equal ${element} Dummy find diff --git a/docs/extending/plugin_api/readme.rst b/docs/extending/plugin_api/readme.rst new file mode 100644 index 000000000..5fce7af27 --- /dev/null +++ b/docs/extending/plugin_api/readme.rst @@ -0,0 +1,16 @@ +Plugin API +========== + +This example demonstrates the plugin API. Example imports plugin which changes +how the `Open Browser`_ works, introduces a new keyword: New Keyword and +plugin changes how the elements are searched from the browser. + +See `Robot Framework test suite`_ and `plugin class`_ for further details. + +Example can be run with:: + + robot adding_plugin.robot + +.. _Open Browser: http://robotframework.org/SeleniumLibrary/SeleniumLibrary.html#Open%20Browser +.. _Robot Framework test suite: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/plugin_api/adding_plugin.robot +.. _plugin class: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/plugin_api/MyPlugin.py diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index a2bb41694..b2b4e15f3 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -317,177 +317,23 @@ class SeleniumLibrary(DynamicCore): = Plugins = - SeleniumLibrary offers plugins as a way to modify, add library keywords and modify some of the internal - functionality without creating new library or hacking the source code. Plugins can be only loaded in the - library import, with the `plugins` argument and SeleniumLibrary does not offer way to unload the - plugins from the SeleniumLibrary. - - Plugins is new SeleniumLibrary 4.0 - - == Importing plugins == - - Importing plugins is similar when importing Robot Framework - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#importing-libraries|libraries]. It - is possible import plugin with using - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-physical-path-to-library|physical path] - or with - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-library-name|plugin name], - exactly in same way as importing libraries in Robot Framework. SeleniumLibrary plugins are searched from the - same - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#module-search-path|module search path] - as Robot Framework searches libraries. It is only possible to import plugins written in Python, other programming - languages or Robot Framework test data is not supported. Like with Robot Framework library imports, plugin - names are case sensitive and spaces are not supported in the plugin name. It is possible to import multiple plugins - at the same time by separating plugins with comma. It is possible to have space before and after the comma. Plugins - are imported in the order they defined in the `plugins` argument. If two or more plugins declare the same keyword - or modify the same method/attribute in the SeleniumLibrary, the last plugin to perform the changes will overwrite - the changes made by other plugins. - - | Library | SeleniumLibrary | plugins=${CURDIR}/MyPlugin.py | # Imports plugin with physical path | - | Library | SeleniumLibrary | plugins=plugins.MyPlugin, plugins.MyOtherPlugin | # Import two plugins with name | - - Generally speaking, plugin are not any different from the classes that are used to implement keyword in the - SeleniumLibrary. Example like with - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/keywords/browsermanagement.py|BrowserManagementKeywords] - class inherits the - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/librarycomponent.py|LibraryComponent] - and uses ``@keyword`` decorator to mark which methods are exposed as keywords. - - == Plugin arguments == - When SeleniumLibrary creates instances from the plugin classes, it will by default initiate the class with a single - argument, called ``ctx`` (context). ``ctx`` is the instance of the SeleniummLibrary and it provides access to the - common methods and attributes used across in the SeleniumLibrary classes. But is recommended to use - wrappers provided by the `LibraryComponent`. - - It is also possible to provide optional arguments to the plugins. Arguments must be separated with a semicolon - from the plugin. SeleniumLibrary will not convert arguments and plugin is responsible for converting the argument - to proper types. - - | Library | SeleniumLibrary | plugins=plugins.Plugin;ArgOne;ArgTwo | # Import two plugins with two arguments: ArgOne and ArgTwo | - - It is possible to provide variable number of arguments and keywords arguments. Named arguments must be defined - first, variable number of arguments as second and keywords arguments as last. All arguments must be separated - with semicolon. Example if plugin __init__ is defined like this: - | class Plugin(LibraryComponent): - | - | def __init__(self, ctx, arg, *varargs, **kwargs): - Then, for example, it is possible to plugin with these arguments: - | Library | SeleniumLibrary | plugins=plugins.Plugin;argument1;varg1;varg2;kw1=kwarg1;kw2=kwarg2 | - Then the ``argument1`` is given the ``arg`` in the ``__init__``. The ``varg1`` and ``varg2`` variable number - arguments are given to the ``*varargs`` argument in the ``__init__``. Finally, the ``kw1=kwarg1`` and - ``kw2=kwarg2`` keyword arguments are given to the ``**kwargs`` in the ``__init__``. As in Python, there can be - zero or more variable number and keyword arguments. - - == Plugin API == - - Plugins must be implemented as Python classes and plugins must inherit the SeleniumLibrary - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/librarycomponent.py|LibraryComponent] - class. Plugin __init__ must support at least one argument: ``ctx``. Also optional arguments are supported, see - `Plugin arguments` for more details how to provide optional arguments to plugins. - - SeleniumLibrary uses Robot Framework - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#dynamic-library-api|dynamic library API]. - The main difference, when compared to libraries using dynamic library API, is that plugins are not responsible - for implementing the dynamic library API. SeleniumLibrary is handling the dynamic library API requirements - towards Robot Framework. For plugins this means that methods that implements keywords, must be decorated - with ``@keyword`` decorator. The ``@keyword`` decorator can be imported from Robot Framework and used in the - following way: - | from robot.api.deco import keyword - | - | class Plugin(LibraryComponent): - | - | @keyword - | def keyword(self): - | # Code here to implement a keyword. - - == Handling failures == - SeleniumLibrary does not suppress exception raised during plugin import or during keywords discovery from the - plugins. In this case the whole SeleniumLibrary import will fail and SeleniumLibrary keywords can not be used - from that import. - - == LibraryComponent == - Although ``ctx`` provides access to the common methods and attributes used in the SeleniumLibrary, the - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/librarycomponent.py|LibraryComponent] - provides more, an easier and IDE friendly access to the common methods and attributes, Example currently - active browser can be found from ``self.ctx.driver``, the ``LibraryComponent`` exposes the browser as: - ``self.driver``. Plugin classes must inherit the ``LibraryComponent``. - - The following methods are available from the ``LibraryComponent`` class: - - | = Method = | = Description = | - | find_element | Finds first element matching ``locator``. | - | find_elements | Find all elements matching ``locator``. | - | is_text_present | Returns True if text is present in the page. | - | is_element_enabled | Returns True if element is enabled. | - | is_visible | Returns True if element is visible. | - | log_source | Calls method defining the `Log Source` keyword. | - | assert_page_contains | Raises AssertionError if element is not found from the page. | - | assert_page_not_contains | Raises AssertionError if element is found from the page. | - | get_timeout | By default returns SeleniumLibrary ``timeout`` argument value. With argument converts string with Robot Framework ``timestr_to_secs`` to seconds. | - | info | Wrapper to ``robot.api.logger.info`` method. | - | debug | Wrapper to ``robot.api.logger.debug`` method. | - | warn | Wrapper to ``robot.api.logger.warn`` method. | - | log | Wrapper to ``robot.api.logger.write`` method. | - - The following attributes are available from the ``LibraryComponent`` class: - - | = Attribute = | = Description = | - | driver | Currently active browser/WebDriver instance in the SeleniumLibrary. | - | drivers | [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/keywords/webdrivertools.py|Cache] for the opened browsers/WebDriver instances. | - | element_finder | Read/write attribute for the [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/locators/elementfinder.py|ElementFinder] instance. | - | ctx | Instance of the SeleniumLibrary. | - | log_dir | Folder where output files are written. | - - See the - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/__init__.py|SeleniumLibrary init], - the - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/librarycomponent.py|LibraryComponent] - and the - [https://github.com/robotframework/SeleniumLibrary/blob/master/src/SeleniumLibrary/base/context.py|ContextAware] - classes for further implementation details. - - == Generating keyword documentation == - To separate keywords which are added or modified by plugins, SeleniumLibrary will add ``plugin`` - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#keyword-tags|keyword tag] - to all keywords added or modified from plugins. When SeleniumLibrary keyword documentation, with plugins, - is generated by - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#library-documentation-tool-libdoc|libdoc] - it is easy to separate keywords which are added or modified by plugins. Keyword documentation can be example - generated by following command: - - | python -m robot.libdoc SeleniumLibrary::plugins=/path/to/Plugin.py ./SeleniumLibraryWithPlugin.html + SeleniumLibrary offers plugins as a way to modify and add library keywords and modify some of the internal + functionality without creating new library or hacking the source code. See + [https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending_seleniumlibrary.rst#Plugins|plugin API] + documentation for further details. + + Plugin API is new SeleniumLibrary 4.0 = EventFiringWebDriver = - The Selenium - [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.event_firing_webdriver.html#module-selenium.webdriver.support.event_firing_webdriver|EventFiringWebDriver] - offers listener API for firing events before and after certain Selenium API calls. - SeleniumLibrary offers support for Selenium ``EventFiringWebDriver`` listener class, by providing possibility - to import the listener class with ``event_firing_webdriver`` argument. Refer to the Selenium - ``EventFiringWebDriver`` documentation which Selenium API methods which can fire events and how the Selenium - listener class should be implemented. + The SeleniumLibrary offers support for + [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.event_firing_webdriver.html#module-selenium.webdriver.support.event_firing_webdriver|EventFiringWebDriver]. + See the Selenium and SeleniumLibrary + [https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending_seleniumlibrary.rst#EventFiringWebDriver|EventFiringWebDriver support] + documentation for futher details. EventFiringWebDriver is new in SeleniumLibrary 4.0 - == Importing listener class == - - Importing Selenium listener class is similar when importing Robot Framework - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#importing-libraries|libraries]. It - is possible import Selenium listener class with using - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-physical-path-to-library|physical path] - or with - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#using-library-name|listener name], - exactly in same way as importing libraries in Robot Framework. Selenium listener class is searched from the same - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#module-search-path|module search path] - as Robot Framework searches libraries. It is only possible to import listener class written in Python, other - programming languages or Robot Framework test data is not supported. Like with Robot Framework library imports, - Selenium listener class name is case sensitive and spaces are not supported in the class name. It is only - possible to import one Selenium listener class and it is not possible to provide arguments for the Selenium - listener class. - - | Library | SeleniumLibrary | event_firing_webdriver=listner.SeleniumListener | # Improts listener with name. | - | Library | SeleniumLibrary | event_firing_webdriver=${CURDIR}/MyListener.py | # Imports listner with physical path. | - = Thread support = SeleniumLibrary is not thread safe. This is mainly due because the underlying