From 54a2b8fbfde6f00c62dfeb3b619b70156c8254e5 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 11 Aug 2023 21:12:33 -0400 Subject: [PATCH 001/122] Initial commit and sketchings of `Print Page As PDF` Keyword Some initial ideas and sketches for the `Print Page As PDF` keyword. I wanted to seewhat it may look like and how it may work. So this is just some preliminary ideas. Seeing a need to handle Print Options, need to document options and what are valid values, how to deal with the return encoded base64 string, etc. --- atest/acceptance/keywords/print_page.robot | 18 ++++++++++++++++++ src/SeleniumLibrary/keywords/screenshot.py | 21 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 atest/acceptance/keywords/print_page.robot diff --git a/atest/acceptance/keywords/print_page.robot b/atest/acceptance/keywords/print_page.robot new file mode 100644 index 000000000..bf377898e --- /dev/null +++ b/atest/acceptance/keywords/print_page.robot @@ -0,0 +1,18 @@ +*** Settings *** +Documentation Suite description +Suite Setup Go To Page "non_ascii.html" +Resource ../resource.robot + +*** Test Cases *** +Print Page As PDF Without Print Options + Print Page As PDF + +Provide Print Options From Module + # ${print_options}= Evaluate sys.modules['selenium.webdriver'].common.print_page_options() sys, selenium.webdriver + ${print_options}= Evaluate selenium.webdriver.common.print_page_options.PrintOptions() + # Set To Dictionary ${print_options} scale 0.5 + # Evaluate ${print_options}.scale=0.5 + # Set Variable ${print_options.scale} 0.5 + # Evaluate ${print_options.scale}=0.5 + Evaluate setattr($print_options, 'scale', 0.5) + Print Page As PDF ${print_options} diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index 628361c8a..57f1d38cc 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -14,10 +14,12 @@ # See the License for the specific language governing permissions and # limitations under the License. import os -from typing import Union +from typing import Optional, Union +from base64 import b64decode from robot.utils import get_link_path from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.common.print_page_options import PrintOptions from SeleniumLibrary.base import LibraryComponent, keyword from SeleniumLibrary.utils.path_formatter import _format_path @@ -235,3 +237,20 @@ def _embed_to_log_as_file(self, path, width): f'', html=True, ) + + @keyword + # def print_page_as_pdf(self, print_options: Optional[PrintOptions]=None,): + # def print_page_as_pdf(self, print_options: Optional[Union[PrintOptions, dict]]=None): + def print_page_as_pdf(self, print_options: Union[PrintOptions, dict, None]=None): + """ Print the current page as a PDF + + """ + if not print_options: + print_options = PrintOptions() + print_options.page_ranges = ['-'] + + base64code = self.driver.print_page(print_options) + pdfdata = b64decode(base64code) + with open('test.pdf', mode='wb') as pdf: + pdf.write(pdfdata) + #self.debug(pdf_str) \ No newline at end of file From c5a101c58aef32e298cc2cba1b8d70d486c77e19 Mon Sep 17 00:00:00 2001 From: Yuri Verweij Date: Sat, 18 Nov 2023 11:50:16 +0100 Subject: [PATCH 002/122] added fix for Page Should Contain Element not accepting lists --- src/SeleniumLibrary/keywords/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py index 94a901dec..0baa1701e 100644 --- a/src/SeleniumLibrary/keywords/element.py +++ b/src/SeleniumLibrary/keywords/element.py @@ -143,7 +143,7 @@ def page_should_contain(self, text: str, loglevel: str = "TRACE"): @keyword def page_should_contain_element( self, - locator: Union[WebElement, str], + locator: Union[WebElement, str, list[Union[WebElement,str]]], message: Optional[str] = None, loglevel: str = "TRACE", limit: Optional[int] = None, From 9c522a28e46f3bc24deed9fefb2a06929e9a79ce Mon Sep 17 00:00:00 2001 From: Yuri Verweij Date: Sat, 18 Nov 2023 11:53:26 +0100 Subject: [PATCH 003/122] added fix for Page Should Contain Element not accepting lists --- src/SeleniumLibrary/keywords/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py index 0baa1701e..92089c37c 100644 --- a/src/SeleniumLibrary/keywords/element.py +++ b/src/SeleniumLibrary/keywords/element.py @@ -143,7 +143,7 @@ def page_should_contain(self, text: str, loglevel: str = "TRACE"): @keyword def page_should_contain_element( self, - locator: Union[WebElement, str, list[Union[WebElement,str]]], + locator: Union[WebElement, str, List[Union[WebElement,str]]], message: Optional[str] = None, loglevel: str = "TRACE", limit: Optional[int] = None, From 684db2d25ccc9e9648c8d6bba082b1c3f4dbd39f Mon Sep 17 00:00:00 2001 From: Yuri Verweij Date: Sat, 18 Nov 2023 14:37:05 +0100 Subject: [PATCH 004/122] added a test for using a WebElement as locator for Page Should Contain Element --- atest/acceptance/locators/locator_parsing.robot | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/atest/acceptance/locators/locator_parsing.robot b/atest/acceptance/locators/locator_parsing.robot index eca57d054..2efb1586f 100644 --- a/atest/acceptance/locators/locator_parsing.robot +++ b/atest/acceptance/locators/locator_parsing.robot @@ -52,6 +52,10 @@ Multiple Locators as a List should work ${locator_list} = Create List id:div_id ${element} id:bar=foo Page Should Contain Element ${locator_list} +WebElement as locator should work + ${element} = Get WebElement id:foo:bar + Page Should Contain Element ${element} + When One Of Locator From Multiple Locators Is Not Found Keyword Fails Run Keyword And Expect Error ... Element with locator 'id:not_here' not found. From 6f9954be92228ec67ac93586190443b74a1f04d4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 17 Feb 2024 09:38:52 -0500 Subject: [PATCH 005/122] Initial retry of creating a Service class Working on an issue with the Options class gave me deeper insight into how it was working. Brought that experience over to the Service class. Thjis is an initial implementation / rewrite of the Service class. It follows more closely the options class. There is just the one test for now and I see I need to bring in the service argument into Open Browser but this feels like a good start. --- atest/acceptance/browser_service.robot | 9 + .../keywords/webdrivertools/webdrivertools.py | 183 ++++++++++++++---- 2 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 atest/acceptance/browser_service.robot diff --git a/atest/acceptance/browser_service.robot b/atest/acceptance/browser_service.robot new file mode 100644 index 000000000..716973c31 --- /dev/null +++ b/atest/acceptance/browser_service.robot @@ -0,0 +1,9 @@ +*** Settings *** +Suite Teardown Close All Browsers +Resource resource.robot +Documentation These tests check the service argument of Open Browser. + +*** Test Cases *** +Browser With Selenium Service As String + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... service=port=1234; executable_path = '/path/to/driver/executable' diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index 16e3d1f48..d4d7da6d2 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -444,16 +444,18 @@ def _get_index(self, alias_or_index): except ValueError: return None -# Temporarily removing as not going to use with initial 4.10.0 hotfixq -# class SeleniumService: -# """ executable_path: str = DEFAULT_EXECUTABLE_PATH, -# port: int = 0, -# log_path: typing.Optional[str] = None, -# service_args: typing.Optional[typing.List[str]] = None, -# env: typing.Optional[typing.Mapping[str, str]] = None, -# **kwargs, - -# executable_path = None, port, service_log_path, service_args, env +class SeleniumService: + """ + + """ + # """ executable_path: str = DEFAULT_EXECUTABLE_PATH, + # port: int = 0, + # log_path: typing.Optional[str] = None, + # service_args: typing.Optional[typing.List[str]] = None, + # env: typing.Optional[typing.Mapping[str, str]] = None, + # **kwargs, + # + # executable_path = None, port, service_log_path, service_args, env # """ # def create(self, browser, # executable_path=None, @@ -464,33 +466,110 @@ def _get_index(self, alias_or_index): # start_error_message=None, # chromium, chrome, edge # quiet=False, reuse_service=False, # safari # ): -# selenium_service = self._import_service(browser) -# # chrome, chromium, firefox, edge -# if any(chromium_based in browser.lower() for chromium_based in ('chromium', 'chrome', 'edge')): -# service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, -# service_args=service_args,env=env,start_error_message=start_error_message -# ) -# return service -# elif 'safari' in browser.lower(): -# service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, -# service_args=service_args,env=env,quiet=quiet,reuse_service=reuse_service -# ) -# return service -# elif 'firefox' in browser.lower(): -# service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, -# service_args=service_args,env=env -# ) -# return service -# else: -# service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, -# service_args=service_args,env=env -# ) -# return service - -# def _import_service(self, browser): -# browser = browser.replace("headless_", "", 1) -# service = importlib.import_module(f"selenium.webdriver.{browser}.service") -# return service.Service + def create(self, browser, service): + if not service: + return None + selenium_service = self._import_service(browser) + if not isinstance(service, str): + return service + + # Throw error is used with remote .. "They cannot be used with a Remote WebDriver session." [ref doc] + attrs = self._parse(service) + selenium_service_inst = selenium_service() + for attr in attrs: + for key in attr: + ser_attr = getattr(selenium_service_inst, key) + if callable(ser_attr): + ser_attr(*attr[key]) + else: + setattr(selenium_service_inst, key, *attr[key]) + return selenium_service_inst + + # # chrome, chromium, firefox, edge + # if any(chromium_based in browser.lower() for chromium_based in ('chromium', 'chrome', 'edge')): + # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, + # service_args=service_args,env=env,start_error_message=start_error_message + # ) + # return service + # elif 'safari' in browser.lower(): + # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, + # service_args=service_args,env=env,quiet=quiet,reuse_service=reuse_service + # ) + # return service + # elif 'firefox' in browser.lower(): + # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, + # service_args=service_args,env=env + # ) + # return service + # else: + # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, + # service_args=service_args,env=env + # ) + # return service + + def _parse(self, service): + result = [] + for item in self._split(service): + try: + result.append(self._parse_to_tokens(item)) + except (ValueError, SyntaxError): + raise ValueError(f'Unable to parse service: "{item}"') + return result + + def _import_service(self, browser): + browser = browser.replace("headless_", "", 1) + # Throw error is used with remote .. "They cannot be used with a Remote WebDriver session." [ref doc] + service = importlib.import_module(f"selenium.webdriver.{browser}.service") + return service.Service + + def _parse_to_tokens(self, item): + result = {} + index, method = self._get_arument_index(item) + if index == -1: + result[item] = [] + return result + if method: + args_as_string = item[index + 1 : -1].strip() + if args_as_string: + args = ast.literal_eval(args_as_string) + else: + args = args_as_string + is_tuple = args_as_string.startswith("(") + else: + args_as_string = item[index + 1 :].strip() + args = ast.literal_eval(args_as_string) + is_tuple = args_as_string.startswith("(") + method_or_attribute = item[:index].strip() + result[method_or_attribute] = self._parse_arguments(args, is_tuple) + return result + + def _parse_arguments(self, argument, is_tuple=False): + if argument == "": + return [] + if is_tuple: + return [argument] + if not is_tuple and isinstance(argument, tuple): + return list(argument) + return [argument] + + def _get_arument_index(self, item): + if "=" not in item: + return item.find("("), True + if "(" not in item: + return item.find("="), False + index = min(item.find("("), item.find("=")) + return index, item.find("(") == index + + def _split(self, service): + split_service = [] + start_position = 0 + tokens = generate_tokens(StringIO(service).readline) + for toknum, tokval, tokpos, _, _ in tokens: + if toknum == token.OP and tokval == ";": + split_service.append(service[start_position : tokpos[1]].strip()) + start_position = tokpos[1] + 1 + split_service.append(options[start_position:]) + return split_service class SeleniumOptions: def create(self, browser, options): @@ -515,6 +594,36 @@ def _import_options(self, browser): options = importlib.import_module(f"selenium.webdriver.{browser}.options") return options.Options + def _parse_to_tokens(self, item): + result = {} + index, method = self._get_arument_index(item) + if index == -1: + result[item] = [] + return result + if method: + args_as_string = item[index + 1 : -1].strip() + if args_as_string: + args = ast.literal_eval(args_as_string) + else: + args = args_as_string + is_tuple = args_as_string.startswith("(") + else: + args_as_string = item[index + 1 :].strip() + args = ast.literal_eval(args_as_string) + is_tuple = args_as_string.startswith("(") + method_or_attribute = item[:index].strip() + result[method_or_attribute] = self._parse_arguments(args, is_tuple) + return result + + def _parse_arguments(self, argument, is_tuple=False): + if argument == "": + return [] + if is_tuple: + return [argument] + if not is_tuple and isinstance(argument, tuple): + return list(argument) + return [argument] + def _parse(self, options): result = [] for item in self._split(options): From da2004f83ad67f94d2f5f50c7608b6ab694c718a Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 18 Feb 2024 10:07:26 -0500 Subject: [PATCH 006/122] Starting to bring service class down into driver creation for various browsers The is just an in ititial commit with some changes which bring the service class down to the point where we create/instantiate the webdrivers for the for the various browsers. Still need some work to actually use what the user provides and to test this code. --- src/SeleniumLibrary/keywords/browsermanagement.py | 10 ++++++++++ .../keywords/webdrivertools/webdrivertools.py | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/SeleniumLibrary/keywords/browsermanagement.py b/src/SeleniumLibrary/keywords/browsermanagement.py index 665ffe89b..57c15087b 100644 --- a/src/SeleniumLibrary/keywords/browsermanagement.py +++ b/src/SeleniumLibrary/keywords/browsermanagement.py @@ -68,6 +68,7 @@ def open_browser( options: Any = None, service_log_path: Optional[str] = None, executable_path: Optional[str] = None, + service: Any = None, ) -> str: """Opens a new browser instance to the optional ``url``. @@ -279,6 +280,10 @@ def open_browser( return index if desired_capabilities: self.warn("desired_capabilities has been deprecated and removed. Please use options to configure browsers as per documentation.") + if service_log_path: + self.warn("service_log_path is being deprecated. Please use service to configure log_output or equivalent service attribute.") + if executable_path: + self.warn("exexcutable_path is being deprecated. Please use service to configure the driver's executable_path as per documentation.") return self._make_new_browser( url, browser, @@ -289,6 +294,7 @@ def open_browser( options, service_log_path, executable_path, + service, ) def _make_new_browser( @@ -302,6 +308,7 @@ def _make_new_browser( options=None, service_log_path=None, executable_path=None, + service=None, ): if remote_url: self.info( @@ -318,6 +325,7 @@ def _make_new_browser( options, service_log_path, executable_path, + service, ) driver = self._wrap_event_firing_webdriver(driver) index = self.ctx.register_driver(driver, alias) @@ -763,6 +771,7 @@ def _make_driver( options=None, service_log_path=None, executable_path=None, + service=None, ): driver = self._webdriver_creator.create_driver( browser=browser, @@ -772,6 +781,7 @@ def _make_driver( options=options, service_log_path=service_log_path, executable_path=executable_path, + service=service, ) driver.set_script_timeout(self.ctx.timeout) driver.implicitly_wait(self.ctx.implicit_wait) diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index d4d7da6d2..5ede4da89 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -69,6 +69,7 @@ def create_driver( options=None, service_log_path=None, executable_path=None, + service=None, ): browser = self._normalise_browser_name(browser) creation_method = self._get_creator_method(browser) @@ -89,6 +90,7 @@ def create_driver( options=options, service_log_path=service_log_path, executable_path=executable_path, + service=service, ) return creation_method( desired_capabilities, @@ -96,6 +98,7 @@ def create_driver( options=options, service_log_path=service_log_path, executable_path=executable_path, + service=service, ) def _get_creator_method(self, browser): @@ -148,6 +151,7 @@ def create_chrome( options=None, service_log_path=None, executable_path="chromedriver", + service=None, ): if remote_url: if not options: @@ -169,6 +173,7 @@ def create_headless_chrome( options=None, service_log_path=None, executable_path="chromedriver", + service=None, ): if not options: options = webdriver.ChromeOptions() From 5ca677d047813d686f00b3e09b634ed8c1738a2b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 18 Feb 2024 21:04:22 -0500 Subject: [PATCH 007/122] Completed initial construct of service class Although I have completed the intial implementation of the service class and it's usage, I have discovered unlike the selenium options most if not all of the attributes must be set on the instantiate of the class. So need to rework the parsing (should check signature instead of attributes) and then construct the class with these parameters. --- atest/acceptance/browser_service.robot | 2 +- .../keywords/webdrivertools/webdrivertools.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/atest/acceptance/browser_service.robot b/atest/acceptance/browser_service.robot index 716973c31..bdce3f4e8 100644 --- a/atest/acceptance/browser_service.robot +++ b/atest/acceptance/browser_service.robot @@ -6,4 +6,4 @@ Documentation These tests check the service argument of Open Browser. *** Test Cases *** Browser With Selenium Service As String Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... service=port=1234; executable_path = '/path/to/driver/executable' + ... service=port=1234; executable_path='/path/to/driver/executable' diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index 5ede4da89..573ff32d1 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -58,7 +58,7 @@ class WebDriverCreator: def __init__(self, log_dir): self.log_dir = log_dir self.selenium_options = SeleniumOptions() - #self.selenium_service = SeleniumService() + self.selenium_service = SeleniumService() def create_driver( self, @@ -76,6 +76,7 @@ def create_driver( desired_capabilities = self._parse_capabilities(desired_capabilities, browser) service_log_path = self._get_log_path(service_log_path) options = self.selenium_options.create(self.browser_names.get(browser), options) + service = self.selenium_service.create(self.browser_names.get(browser), service) if service_log_path: logger.info(f"Browser driver log file created to: {service_log_path}") self._create_directory(service_log_path) @@ -160,7 +161,8 @@ def create_chrome( if not executable_path: executable_path = self._get_executable_path(webdriver.chrome.service.Service) log_method = self._get_log_method(ChromeService, service_log_path) - service = ChromeService(executable_path=executable_path, **log_method) + if not service: + service = ChromeService(executable_path=executable_path, **log_method) return webdriver.Chrome( options=options, service=service, @@ -573,7 +575,7 @@ def _split(self, service): if toknum == token.OP and tokval == ";": split_service.append(service[start_position : tokpos[1]].strip()) start_position = tokpos[1] + 1 - split_service.append(options[start_position:]) + split_service.append(service[start_position:]) return split_service class SeleniumOptions: From 3cbc641bd9276b9a7b99cf0174bcf1eb563455a8 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 19 Apr 2024 09:19:44 -0400 Subject: [PATCH 008/122] Back to dev version --- src/SeleniumLibrary/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 24ae59b3f..fe550aec5 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -52,7 +52,7 @@ from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay -__version__ = "6.3.0" +__version__ = "6.4.0.dev1" class SeleniumLibrary(DynamicCore): From 15b57e96ef77cea243695d04b9a0455ba638fa82 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 28 Apr 2024 14:13:55 +0300 Subject: [PATCH 009/122] Fix doc to contains correct link syntax --- src/SeleniumLibrary/keywords/expectedconditions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/keywords/expectedconditions.py b/src/SeleniumLibrary/keywords/expectedconditions.py index 7b896fd6c..c0272ae75 100644 --- a/src/SeleniumLibrary/keywords/expectedconditions.py +++ b/src/SeleniumLibrary/keywords/expectedconditions.py @@ -26,7 +26,7 @@ def wait_for_expected_condition(self, condition: string, *args, timeout: Optiona The condition must be one of selenium's expected condition which can be found within the selenium - [https://www.selenium.dev/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.expected_conditions.html#module-selenium.webdriver.support.expected_conditions Python API] + [https://www.selenium.dev/selenium/docs/api/py/webdriver_support/selenium.webdriver.support.expected_conditions.html#module-selenium.webdriver.support.expected_conditions|Python API] documentation. The expected condition can written as snake_case (ex title_is) or it can be space delimited (ex Title Is). Some conditions require additional arguments or ``args`` which should From 8d9940cf92803c8f71436c59e4fc80307554f2d7 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 28 Apr 2024 16:19:06 -0400 Subject: [PATCH 010/122] Added warning to `Page Should Contain` keyword Added warning about the deselection of the frame reference. Fixes #1894 --- src/SeleniumLibrary/keywords/element.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py index d277791f5..2eaa3835f 100644 --- a/src/SeleniumLibrary/keywords/element.py +++ b/src/SeleniumLibrary/keywords/element.py @@ -132,6 +132,12 @@ def page_should_contain(self, text: str, loglevel: str = "TRACE"): argument. Valid log levels are ``TRACE`` (default), ``DEBUG``, ``INFO``, ``WARN``, and ``NONE``. If the log level is ``NONE`` or below the current active log level the source will not be logged. + + !! WARNING !! If you have an iframe selected, `Page Should Contain` + will reset the frame reference back to the main frame. This is due + to the fact that is searches for the ``text`` in all frames. To locate + an element in an iframe after calling `Page Should Contian` one needs + to (re)select the frame. """ if not self._page_contains(text): self.ctx.log_source(loglevel) From 5ca87fb3be5152b0bc188f0c09ee82be3bfdbcbc Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 28 Apr 2024 22:03:19 -0400 Subject: [PATCH 011/122] Starting the removal of Selenium2Library - Moved the `Versions` and `History` sections to VERSIONS.rst - Moved instructions for installing legacy Selenium2Library over to VERSIONS.rst - Removed links to old SeleniumLibrary and Selenium2Library project while still linking under VERSIONS.rst - Updated selenium and Python versions which the current SeleniumLibrary works with - Removed mailing list from listed community channels --- README.rst | 95 +++++----------------------------------------------- VERSIONS.rst | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 86 deletions(-) create mode 100644 VERSIONS.rst diff --git a/README.rst b/README.rst index 6948a9ea5..633db7038 100644 --- a/README.rst +++ b/README.rst @@ -10,14 +10,14 @@ SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes the Selenium_ tool internally. The project is hosted on GitHub_ and downloads can be found from PyPI_. -SeleniumLibrary works with Selenium 3 and 4. It supports Python 3.6 or +SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 or newer. In addition to the normal Python_ interpreter, it works also with PyPy_. -SeleniumLibrary is based on the `old SeleniumLibrary`_ that was forked to -Selenium2Library_ and then later renamed back to SeleniumLibrary. -See the Versions_ and History_ sections below for more information about -different versions and the overall project history. +SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to +Selenium2Library and then later renamed back to SeleniumLibrary. +See the `VERSIONS.rst`_ for more information about different versions and the +overall project history. .. image:: https://img.shields.io/pypi/v/robotframework-seleniumlibrary.svg?label=version :target: https://pypi.python.org/pypi/robotframework-seleniumlibrary @@ -48,23 +48,8 @@ versions, but you still need to install `browser drivers`_ separately. The ``--upgrade`` option can be omitted when installing the library for the first time. -Those migrating from Selenium2Library_ can install SeleniumLibrary so that -it is exposed also as Selenium2Library:: - - pip install --upgrade robotframework-selenium2library - -The above command installs the normal SeleniumLibrary as well as a new -Selenium2Library version that is just a thin wrapper to SeleniumLibrary. -That allows importing Selenium2Library in tests while migrating to -SeleniumLibrary. - -To install the last legacy Selenium2Library_ version, use this command instead:: - - pip install robotframework-selenium2library==1.8.0 - -With recent versions of ``pip`` it is possible to install directly from the -GitHub_ repository. To install latest source from the master branch, use -this command:: +It is possible to install directly from the GitHub_ repository. To install +latest source from the master branch, use this command:: pip install git+https://github.com/robotframework/SeleniumLibrary.git @@ -197,7 +182,6 @@ Community If the provided documentation is not enough, there are various community channels available: -- `robotframework-users`_ mailing list - ``#seleniumlibrary`` and ``#seleniumlibrary-dev`` channels in Robot Framework `Slack community`_ - `Robot Framework forum`_ has channel for SeleniumLibrary. @@ -205,69 +189,6 @@ available: requests - `Other community channels`_ including paid support -Versions --------- - -SeleniumLibrary has over the years lived under SeleniumLibrary and -Selenium2Library names and different library versions have supported -different Selenium and Python versions. This is summarized in the table -below and the History_ section afterwards explains the project history -a bit more. - -================================== ========================== ========================== =============== - Project Selenium Version Python Version Comment -================================== ========================== ========================== =============== -SeleniumLibrary 2.9.2 and earlier Selenium 1 and 2 Python 2.5-2.7 The original SeleniumLibrary using Selenium RC API. -Selenium2Library 1.8.0 and earlier Selenium 2 and 3 Python 2.6-2.7 Fork of SeleniumLibrary using Selenium WebDriver API. -SeleniumLibrary 3.0 and 3.1 Selenium 2 and 3 Python 2.7 and 3.3+ Selenium2Library renamed and with Python 3 support and new architecture. -SeleniumLibrary 3.2 Selenium 3 Python 2.7 and 3.4+ Drops Selenium 2 support. -SeleniumLibrary 4.0 Selenium 3 Python 2.7 and 3.4+ Plugin API and support for event friging webdriver. -SeleniumLibrary 4.1 Selenium 3 Python 2.7 and 3.5+ Drops Python 3.4 support. -SeleniumLibrary 4.2 Selenium 3 Python 2.7 and 3.5+ Supports only Selenium 3.141.0 or newer. -SeleniumLibrary 4.4 Selenium 3 and 4 Python 2.7 and 3.6+ New PythonLibCore and dropped Python 3.5 support. -SeleniumLibrary 5.0 Selenium 3 and 4 Python 3.6+ Python 2 and Jython support is dropped. -SeleniumLibrary 5.1 Selenium 3 and 4 Python 3.6+ Robot Framework 3.1 support is dropped. -Selenium2Library 3.0 Depends on SeleniumLibrary Depends on SeleniumLibrary Thin wrapper for SeleniumLibrary 3.0 to ease transition. -================================== ========================== ========================== =============== - -History -------- - -SeleniumLibrary originally used the Selenium Remote Controller (RC) API. -When Selenium 2 was introduced with the new but backwards incompatible -WebDriver API, SeleniumLibrary kept using Selenium RC and separate -Selenium2Library using WebDriver was forked. These projects contained -mostly the same keywords and in most cases Selenium2Library was a drop-in -replacement for SeleniumLibrary. - -Over the years development of the old SeleniumLibrary stopped and also -the Selenium RC API it used was deprecated. Selenium2Library was developed -further and replaced the old library as the de facto web testing library -for Robot Framework. - -When Selenium 3 was released in 2016, it was otherwise backwards compatible -with Selenium 2, but the deprecated Selenium RC API was removed. This had two -important effects: - -- The old SeleniumLibrary could not anymore be used with new Selenium versions. - This project was pretty much dead. -- Selenium2Library was badly named as it supported Selenium 3 just fine. - This project needed a new name. - -At the same time when Selenium 3 was released, Selenium2Library was going -through larger architecture changes in order to ease future maintenance and -to make adding Python 3 support easier. With all these big internal and -external changes, it made sense to rename Selenium2Library back to -SeleniumLibrary. This decision basically meant following changes: - -- Create separate repository for the `old SeleniumLibrary`_ to preserve - its history since Selenium2Library was forked. -- Rename Selenium2Library project and the library itself to SeleniumLibrary_. -- Add new Selenium2Library_ project to ease transitioning from Selenium2Library - to SeleniumLibrary. - -Going forward, all new development will happen in the new SeleniumLibrary -project. .. _Robot Framework: https://robotframework.org .. _Selenium: https://www.seleniumhq.org/ @@ -291,3 +212,5 @@ project. .. _Robot Framework forum: https://forum.robotframework.org/ .. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues .. _Other community channels: https://robotframework.org/#community +.. _VERSIONS.rst: https://github.com/robotframework/SeleniumLibrary/blob/master/VERSIONS.rst + diff --git a/VERSIONS.rst b/VERSIONS.rst new file mode 100644 index 000000000..289736971 --- /dev/null +++ b/VERSIONS.rst @@ -0,0 +1,72 @@ +Versions +-------- + +SeleniumLibrary has over the years lived under SeleniumLibrary and +Selenium2Library names and different library versions have supported +different Selenium and Python versions. This is summarized in the table +below and the History_ section afterwards explains the project history +a bit more. + +================================== ========================== ========================== =============== + Project Selenium Version Python Version Comment +================================== ========================== ========================== =============== +SeleniumLibrary 2.9.2 and earlier Selenium 1 and 2 Python 2.5-2.7 The original SeleniumLibrary using Selenium RC API. +Selenium2Library 1.8.0 and earlier Selenium 2 and 3 Python 2.6-2.7 Fork of SeleniumLibrary using Selenium WebDriver API. +SeleniumLibrary 3.0 and 3.1 Selenium 2 and 3 Python 2.7 and 3.3+ Selenium2Library renamed and with Python 3 support and new architecture. +SeleniumLibrary 3.2 Selenium 3 Python 2.7 and 3.4+ Drops Selenium 2 support. +SeleniumLibrary 4.0 Selenium 3 Python 2.7 and 3.4+ Plugin API and support for event friging webdriver. +SeleniumLibrary 4.1 Selenium 3 Python 2.7 and 3.5+ Drops Python 3.4 support. +SeleniumLibrary 4.2 Selenium 3 Python 2.7 and 3.5+ Supports only Selenium 3.141.0 or newer. +SeleniumLibrary 4.4 Selenium 3 and 4 Python 2.7 and 3.6+ New PythonLibCore and dropped Python 3.5 support. +SeleniumLibrary 5.0 Selenium 3 and 4 Python 3.6+ Python 2 and Jython support is dropped. +SeleniumLibrary 5.1 Selenium 3 and 4 Python 3.6+ Robot Framework 3.1 support is dropped. +Selenium2Library 3.0 Depends on SeleniumLibrary Depends on SeleniumLibrary Thin wrapper for SeleniumLibrary 3.0 to ease transition. +================================== ========================== ========================== =============== + +History +------- + +SeleniumLibrary originally used the Selenium Remote Controller (RC) API. +When Selenium 2 was introduced with the new but backwards incompatible +WebDriver API, SeleniumLibrary kept using Selenium RC and separate +Selenium2Library using WebDriver was forked. These projects contained +mostly the same keywords and in most cases Selenium2Library was a drop-in +replacement for SeleniumLibrary. + +Over the years development of the old SeleniumLibrary stopped and also +the Selenium RC API it used was deprecated. Selenium2Library was developed +further and replaced the old library as the de facto web testing library +for Robot Framework. + +When Selenium 3 was released in 2016, it was otherwise backwards compatible +with Selenium 2, but the deprecated Selenium RC API was removed. This had two +important effects: + +- The old SeleniumLibrary could not anymore be used with new Selenium versions. + This project was pretty much dead. +- Selenium2Library was badly named as it supported Selenium 3 just fine. + This project needed a new name. + +At the same time when Selenium 3 was released, Selenium2Library was going +through larger architecture changes in order to ease future maintenance and +to make adding Python 3 support easier. With all these big internal and +external changes, it made sense to rename Selenium2Library back to +SeleniumLibrary. This decision basically meant following changes: + +- Create separate repository for the `old SeleniumLibrary`_ to preserve + its history since Selenium2Library was forked. +- Rename Selenium2Library project and the library itself to SeleniumLibrary_. +- Add new Selenium2Library_ project to ease transitioning from Selenium2Library + to SeleniumLibrary. + +All new development is happenning in the SeleniumLibrary project. + +Installation of legacy Selenium2Library +--------------------------------------- +To install the last legacy Selenium2Library_ version, use this command:: + + pip install robotframework-selenium2library==1.8.0 + +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium2Library: https://github.com/robotframework/Selenium2Library +.. _Old SeleniumLibrary: https://github.com/robotframework/OldSeleniumLibrary From 39bd3f6b5a987718c0ea0ec2d5c78766ead3ff40 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 28 Apr 2024 22:09:35 -0400 Subject: [PATCH 012/122] Updated required and supported Python versions within setup.py --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index e2a6492ff..39595e00e 100755 --- a/setup.py +++ b/setup.py @@ -13,9 +13,10 @@ Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 -Programming Language :: Python :: 3.6 -Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 +Programming Language :: Python :: 3.9 +Programming Language :: Python :: 3.10 +Programming Language :: Python :: 3.11 Programming Language :: Python :: 3 :: Only Topic :: Software Development :: Testing Framework :: Robot Framework @@ -40,7 +41,7 @@ keywords = 'robotframework testing testautomation selenium webdriver web', platforms = 'any', classifiers = CLASSIFIERS, - python_requires = '>=3.6, <4', + python_requires = '>=3.8, <3.12', install_requires = REQUIREMENTS, package_dir = {'': 'src'}, packages = find_packages('src'), From 02fc8631bd0f68d45f8a87566d82957d98103709 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 28 Apr 2024 22:16:15 -0400 Subject: [PATCH 013/122] Removed legacy links --- README.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.rst b/README.rst index 633db7038..68b3405d3 100644 --- a/README.rst +++ b/README.rst @@ -193,20 +193,15 @@ available: .. _Robot Framework: https://robotframework.org .. _Selenium: https://www.seleniumhq.org/ .. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary -.. _Selenium2Library: https://github.com/robotframework/Selenium2Library -.. _Old SeleniumLibrary: https://github.com/robotframework/OldSeleniumLibrary .. _pip: http://pip-installer.org .. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary .. _GitHub: https://github.com/robotframework/SeleniumLibrary .. _Keyword Documentation: https://robotframework.org/SeleniumLibrary/SeleniumLibrary.html .. _Python: https://python.org .. _PyPy: https://pypy.org -.. _Jython: https://jython.org/ -.. _IronPython: https://ironpython.net/ .. _demo project: https://github.com/robotframework/WebDemo .. _Robot Framework User Guide: https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html .. _Robot Framework installation instructions: https://github.com/robotframework/robotframework/blob/master/INSTALL.rst -.. _robotframework-users: https://groups.google.com/group/robotframework-users .. _extending documentation: https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending.rst .. _Slack community: https://robotframework-slack-invite.herokuapp.com .. _Robot Framework forum: https://forum.robotframework.org/ From b462eabcad0f6888b2da293fb13f549fb10c38f2 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 29 Apr 2024 17:33:28 -0400 Subject: [PATCH 014/122] Switched from single print_options argument to individual options --- src/SeleniumLibrary/keywords/screenshot.py | 60 +++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index 57f1d38cc..3cc87d569 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -241,16 +241,64 @@ def _embed_to_log_as_file(self, path, width): @keyword # def print_page_as_pdf(self, print_options: Optional[PrintOptions]=None,): # def print_page_as_pdf(self, print_options: Optional[Union[PrintOptions, dict]]=None): - def print_page_as_pdf(self, print_options: Union[PrintOptions, dict, None]=None): + # def print_page_as_pdf(self, print_options: Union[PrintOptions, dict, None]=None): + # """ Print the current page as a PDF + # + # """ + # if not print_options: + # print_options = PrintOptions() + # print_options.page_ranges = ['-'] + def print_page_as_pdf(self, + background=None, + margin_bottom=None, + margin_left=None, + margin_right=None, + margin_top=None, + orientation=None, + page_height=None, + page_ranges=['-'], + page_width=None, + scale=None, + shrink_to_fit=None, + path_to_file=None, + ): """ Print the current page as a PDF + ``page_ranges`` defaults to `['-']` or "all" pages. ``page_ranges`` takes a list of + strings indicating the ranges. + + The page size defaults to 21.59 for ``page_width`` and 27.94 for ``page_height``. + This is the equivalent size of US-Letter. The assumed units on these parameters + is centimeters. + + The default margin for top, left, bottom, right is `1`. The assumed units on + these parameters is centimeters. + + The default ``orientation`` is `portrait`. ``orientation`` can be either `portrait` + or `landscape`. + + The default ``scale`` is `1`. ``scale`` must be greater than or equal to `0.1` and + less than or equal to `2`. + + ``background`` and ``scale_to_fite`` can be either `${True}` or `${False}`.. + + If all print options are None then a pdf will failed to print silently.s """ - if not print_options: - print_options = PrintOptions() - print_options.page_ranges = ['-'] + + print_options = PrintOptions() + print_options.background = background + print_options.margin_bottom = margin_bottom + print_options.margin_left = margin_left + print_options.margin_right = margin_right + print_options.margin_top = margin_top + print_options.orientation = orientation + print_options.page_height = page_height + print_options.page_ranges = page_ranges + print_options.page_width = page_width + print_options.scale = scale + print_options.shrink_to_fit = shrink_to_fit base64code = self.driver.print_page(print_options) pdfdata = b64decode(base64code) with open('test.pdf', mode='wb') as pdf: - pdf.write(pdfdata) - #self.debug(pdf_str) \ No newline at end of file + pdf.write(pdfdata) \ No newline at end of file From c083b0f926e40aaadd7300147c4f2e1ba2f29535 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 29 Apr 2024 20:29:44 -0400 Subject: [PATCH 015/122] Removed None type for locator where is shouldn't be --- src/SeleniumLibrary/keywords/screenshot.py | 2 +- src/SeleniumLibrary/keywords/tableelement.py | 14 +++++++------- src/SeleniumLibrary/keywords/waiting.py | 14 +++++++------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index 628361c8a..db84c2861 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -128,7 +128,7 @@ def _capture_page_screen_to_log(self): @keyword def capture_element_screenshot( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], filename: str = DEFAULT_FILENAME_ELEMENT, ) -> str: """Captures a screenshot from the element identified by ``locator`` and embeds it into log file. diff --git a/src/SeleniumLibrary/keywords/tableelement.py b/src/SeleniumLibrary/keywords/tableelement.py index ce4a74836..e054e9d77 100644 --- a/src/SeleniumLibrary/keywords/tableelement.py +++ b/src/SeleniumLibrary/keywords/tableelement.py @@ -25,7 +25,7 @@ class TableElementKeywords(LibraryComponent): @keyword def get_table_cell( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], row: int, column: int, loglevel: str = "TRACE", @@ -89,7 +89,7 @@ def _get_rows(self, locator, count): @keyword def table_cell_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], row: int, column: int, expected: str, @@ -112,7 +112,7 @@ def table_cell_should_contain( @keyword def table_column_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], column: int, expected: str, loglevel: str = "TRACE", @@ -143,7 +143,7 @@ def table_column_should_contain( @keyword def table_footer_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], expected: str, loglevel: str = "TRACE", ): @@ -168,7 +168,7 @@ def table_footer_should_contain( @keyword def table_header_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], expected: str, loglevel: str = "TRACE", ): @@ -193,7 +193,7 @@ def table_header_should_contain( @keyword def table_row_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], row: int, expected: str, loglevel: str = "TRACE", @@ -224,7 +224,7 @@ def table_row_should_contain( @keyword def table_should_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], expected: str, loglevel: str = "TRACE", ): diff --git a/src/SeleniumLibrary/keywords/waiting.py b/src/SeleniumLibrary/keywords/waiting.py index 62c84c9f2..eeec6756e 100644 --- a/src/SeleniumLibrary/keywords/waiting.py +++ b/src/SeleniumLibrary/keywords/waiting.py @@ -222,7 +222,7 @@ def wait_until_page_does_not_contain( @keyword def wait_until_page_contains_element( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], timeout: Optional[timedelta] = None, error: Optional[str] = None, limit: Optional[int] = None, @@ -260,7 +260,7 @@ def wait_until_page_contains_element( @keyword def wait_until_page_does_not_contain_element( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], timeout: Optional[timedelta] = None, error: Optional[str] = None, limit: Optional[int] = None, @@ -298,7 +298,7 @@ def wait_until_page_does_not_contain_element( @keyword def wait_until_element_is_visible( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], timeout: Optional[timedelta] = None, error: Optional[str] = None, ): @@ -321,7 +321,7 @@ def wait_until_element_is_visible( @keyword def wait_until_element_is_not_visible( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], timeout: Optional[timedelta] = None, error: Optional[str] = None, ): @@ -344,7 +344,7 @@ def wait_until_element_is_not_visible( @keyword def wait_until_element_is_enabled( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], timeout: Optional[timedelta] = None, error: Optional[str] = None, ): @@ -372,7 +372,7 @@ def wait_until_element_is_enabled( @keyword def wait_until_element_contains( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], text: str, timeout: Optional[timedelta] = None, error: Optional[str] = None, @@ -396,7 +396,7 @@ def wait_until_element_contains( @keyword def wait_until_element_does_not_contain( self, - locator: Union[WebElement, None, str], + locator: Union[WebElement, str], text: str, timeout: Optional[timedelta] = None, error: Optional[str] = None, From 680ce1d62a8d5b43fae19c9eb4bf652daa26339b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Tue, 30 Apr 2024 19:36:58 -0400 Subject: [PATCH 016/122] Updated work on adding service class Some minor changes which I want to store away. Starting to see the final version of this code. Also started to add a test case copied from the options test. All this is a work in progress. --- .../multiple_browsers_service.robot | 53 +++++++++++++++++++ .../keywords/webdrivertools/webdrivertools.py | 11 ++++ 2 files changed, 64 insertions(+) create mode 100644 atest/acceptance/multiple_browsers_service.robot diff --git a/atest/acceptance/multiple_browsers_service.robot b/atest/acceptance/multiple_browsers_service.robot new file mode 100644 index 000000000..2272effde --- /dev/null +++ b/atest/acceptance/multiple_browsers_service.robot @@ -0,0 +1,53 @@ +*** Settings *** +Suite Teardown Close All Browsers +# Library ../resources/testlibs/get_selenium_options.py +Resource resource.robot +# Force Tags Known Issue Firefox Known Issue Safari Known Issue Internet Explorer +Documentation Creating test which would work on all browser is not possible. +... These tests are for Chrome only. + +*** Test Cases *** +Chrome Browser With Chrome Service As String + [Documentation] + ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* + # ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... service=executable_path=/usr/local/bin; port=9999 + +Chrome Browser With Chrome Service As String With service_args As List + [Documentation] + ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* + ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* + ... LOG 1:14 DEBUG GLOB: *"--headless=new"* + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... service=service_args=['--append-log', '--readable-timestamp'], log_output=log_path) + +Chrome Browser With Selenium Options With Complex Object + [Tags] NoGrid + [Documentation] + ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* + ... LOG 1:14 DEBUG GLOB: *"mobileEmulation": {"deviceName": "Galaxy S5"* + ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... options=add_argument ( "--disable-dev-shm-usage" ) ; add_experimental_option( "mobileEmulation" , { 'deviceName' : 'Galaxy S5'}) + +Chrome Browser With Selenium Options Object + [Documentation] + ... LOG 2:14 DEBUG GLOB: *"goog:chromeOptions"* + ... LOG 2:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* + ${options} = Get Chrome Options + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... desired_capabilities=${DESIRED_CAPABILITIES} options=${options} + +Chrome Browser With Selenium Options Invalid Method + Run Keyword And Expect Error AttributeError: 'Options' object has no attribute 'not_here_method' + ... Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... desired_capabilities=${DESIRED_CAPABILITIES} options=not_here_method("arg1") + + +Chrome Browser With Selenium Options Argument With Semicolon + [Documentation] + ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* + ... LOG 1:14 DEBUG GLOB: *["has;semicolon"* + Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument("has;semicolon") diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index 573ff32d1..918d98ded 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -578,6 +578,17 @@ def _split(self, service): split_service.append(service[start_position:]) return split_service + def _dualsplit(self, service_or_attr, splittok): + split_string = [] + start_position = 0 + tokens = generate_tokens(StringIO(service_or_attr).readline) + for toknum, tokval, tokpos, _, _ in tokens: + if toknum == token.OP and tokval == splittok: + split_string.append(service_or_attr[start_position : tokpos[1]].strip()) + start_position = tokpos[1] + 1 + split_string.append(service_or_attr[start_position:]) + return split_string + class SeleniumOptions: def create(self, browser, options): if not options: From 623be5ada01262f4c13d8595caf20bd87f1f8da4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Tue, 30 Apr 2024 21:12:30 -0400 Subject: [PATCH 017/122] Almost complete buit still an error with instatiating the service class It is almost complete but still something wrong with how I am instatiating the service class with the parsed arguments. Cleaned up the Service class and added a test case where I wasn't trying to set the driver path (which is a bad idea to use a made up one because the test fails in the the chromedriver wouldn't start). --- .../multiple_browsers_service.robot | 2 +- .../keywords/webdrivertools/webdrivertools.py | 74 ++++++++++--------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/atest/acceptance/multiple_browsers_service.robot b/atest/acceptance/multiple_browsers_service.robot index 2272effde..f7519fe43 100644 --- a/atest/acceptance/multiple_browsers_service.robot +++ b/atest/acceptance/multiple_browsers_service.robot @@ -20,7 +20,7 @@ Chrome Browser With Chrome Service As String With service_args As List ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* ... LOG 1:14 DEBUG GLOB: *"--headless=new"* Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... service=service_args=['--append-log', '--readable-timestamp'], log_output=log_path) + ... service=service_args=['--append-log', '--readable-timestamp']; log_output='.' Chrome Browser With Selenium Options With Complex Object [Tags] NoGrid diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index 918d98ded..5db13622f 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -482,43 +482,26 @@ def create(self, browser, service): # Throw error is used with remote .. "They cannot be used with a Remote WebDriver session." [ref doc] attrs = self._parse(service) - selenium_service_inst = selenium_service() - for attr in attrs: - for key in attr: - ser_attr = getattr(selenium_service_inst, key) - if callable(ser_attr): - ser_attr(*attr[key]) - else: - setattr(selenium_service_inst, key, *attr[key]) + # verify attribute a member of service class parameters + service_parameters = inspect.signature(selenium_service).parameters + for key in attrs: + if key not in service_parameters: + service_module = '.'.join((selenium_service.__module__, selenium_service.__qualname__)) + raise ValueError(f"{key} is not a member of {service_module} Service class") + selenium_service_inst = selenium_service(**attrs) return selenium_service_inst - # # chrome, chromium, firefox, edge - # if any(chromium_based in browser.lower() for chromium_based in ('chromium', 'chrome', 'edge')): - # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, - # service_args=service_args,env=env,start_error_message=start_error_message - # ) - # return service - # elif 'safari' in browser.lower(): - # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, - # service_args=service_args,env=env,quiet=quiet,reuse_service=reuse_service - # ) - # return service - # elif 'firefox' in browser.lower(): - # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, - # service_args=service_args,env=env - # ) - # return service - # else: - # service = selenium_service(executable_path=executable_path, port=port,log_path=service_log_path, - # service_args=service_args,env=env - # ) - # return service - def _parse(self, service): - result = [] - for item in self._split(service): + """The service argument parses slightly different than the options argument. As of + Selenium v4.20.0, all the service items are arguments applied to the service class + instantiation. Thus each item is split instead parsed as done with options. + """ + result = {} + for item in self._split(service,';'): try: - result.append(self._parse_to_tokens(item)) + attr, val = self._split(item, '=') + result[attr]=val + # result.append(self._parse_to_tokens(item)) except (ValueError, SyntaxError): raise ValueError(f'Unable to parse service: "{item}"') return result @@ -550,6 +533,27 @@ def _parse_to_tokens(self, item): result[method_or_attribute] = self._parse_arguments(args, is_tuple) return result + def _old_parse_to_tokens(self, item): + result = {} + index, method = self._get_arument_index(item) + if index == -1: + result[item] = [] + return result + if method: + args_as_string = item[index + 1 : -1].strip() + if args_as_string: + args = ast.literal_eval(args_as_string) + else: + args = args_as_string + is_tuple = args_as_string.startswith("(") + else: + args_as_string = item[index + 1 :].strip() + args = ast.literal_eval(args_as_string) + is_tuple = args_as_string.startswith("(") + method_or_attribute = item[:index].strip() + result[method_or_attribute] = self._parse_arguments(args, is_tuple) + return result + def _parse_arguments(self, argument, is_tuple=False): if argument == "": return [] @@ -567,7 +571,7 @@ def _get_arument_index(self, item): index = min(item.find("("), item.find("=")) return index, item.find("(") == index - def _split(self, service): + def _oldsplit(self, service): split_service = [] start_position = 0 tokens = generate_tokens(StringIO(service).readline) @@ -578,7 +582,7 @@ def _split(self, service): split_service.append(service[start_position:]) return split_service - def _dualsplit(self, service_or_attr, splittok): + def _split(self, service_or_attr, splittok): split_string = [] start_position = 0 tokens = generate_tokens(StringIO(service_or_attr).readline) From 5d6ad83da34b785714b2eb269eced0296e0db51b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 4 May 2024 21:04:35 -0400 Subject: [PATCH 018/122] Have working code for adding Service as string Update some of the atests. Need to add some unit tests and make sure service class is getting passed in. Need to deprecate and guide users away from service_log_path and executable_path. --- .../multiple_browsers_service.robot | 67 ++++++-------- atest/resources/testlibs/get_driver_path.py | 47 ++++++++++ .../keywords/webdrivertools/webdrivertools.py | 91 +------------------ 3 files changed, 77 insertions(+), 128 deletions(-) create mode 100644 atest/resources/testlibs/get_driver_path.py diff --git a/atest/acceptance/multiple_browsers_service.robot b/atest/acceptance/multiple_browsers_service.robot index f7519fe43..d92445165 100644 --- a/atest/acceptance/multiple_browsers_service.robot +++ b/atest/acceptance/multiple_browsers_service.robot @@ -1,6 +1,6 @@ *** Settings *** Suite Teardown Close All Browsers -# Library ../resources/testlibs/get_selenium_options.py +Library ../resources/testlibs/get_driver_path.py Resource resource.robot # Force Tags Known Issue Firefox Known Issue Safari Known Issue Internet Explorer Documentation Creating test which would work on all browser is not possible. @@ -9,45 +9,36 @@ Documentation Creating test which would work on all browser is not possible. *** Test Cases *** Chrome Browser With Chrome Service As String [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - # ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... service=executable_path=/usr/local/bin; port=9999 + ... LOG 2:2 DEBUG STARTS: Started executable: + ... LOG 2:3 DEBUG GLOB: POST*/session* + ${driver_path}= Get Driver Path Chrome + Open Browser ${FRONT PAGE} Chrome remote_url=${REMOTE_URL} + ... service=executable_path='${driver_path}' Chrome Browser With Chrome Service As String With service_args As List - [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* - ... LOG 1:14 DEBUG GLOB: *"--headless=new"* - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... service=service_args=['--append-log', '--readable-timestamp']; log_output='.' + Open Browser ${FRONT PAGE} Chrome remote_url=${REMOTE_URL} + ... service=service_args=['--append-log', '--readable-timestamp']; log_output='${OUTPUT_DIR}/chromedriverlog.txt' + File Should Exist ${OUTPUT_DIR}/chromedriverlog.txt + # ... service=service_args=['--append-log', '--readable-timestamp']; log_output='./' + # ... service=service_args=['--append-log', '--readable-timestamp'] -Chrome Browser With Selenium Options With Complex Object - [Tags] NoGrid - [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 1:14 DEBUG GLOB: *"mobileEmulation": {"deviceName": "Galaxy S5"* - ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... options=add_argument ( "--disable-dev-shm-usage" ) ; add_experimental_option( "mobileEmulation" , { 'deviceName' : 'Galaxy S5'}) - -Chrome Browser With Selenium Options Object +Firefox Browser With Firefox Service As String [Documentation] - ... LOG 2:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 2:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* - ${options} = Get Chrome Options - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... desired_capabilities=${DESIRED_CAPABILITIES} options=${options} - -Chrome Browser With Selenium Options Invalid Method - Run Keyword And Expect Error AttributeError: 'Options' object has no attribute 'not_here_method' - ... Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... desired_capabilities=${DESIRED_CAPABILITIES} options=not_here_method("arg1") - + ... LOG 2:2 DEBUG STARTS: Started executable: + ... LOG 2:3 DEBUG GLOB: POST*/session* + ${driver_path}= Get Driver Path Firefox + Open Browser ${FRONT PAGE} Firefox remote_url=${REMOTE_URL} + ... service=executable_path='${driver_path}' -Chrome Browser With Selenium Options Argument With Semicolon - [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 1:14 DEBUG GLOB: *["has;semicolon"* - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument("has;semicolon") +#Chrome Browser With Selenium Options Invalid Method +# Run Keyword And Expect Error AttributeError: 'Options' object has no attribute 'not_here_method' +# ... Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} +# ... desired_capabilities=${DESIRED_CAPABILITIES} options=not_here_method("arg1") +# +# +#Chrome Browser With Selenium Options Argument With Semicolon +# [Documentation] +# ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* +# ... LOG 1:14 DEBUG GLOB: *["has;semicolon"* +# Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} +# ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument("has;semicolon") diff --git a/atest/resources/testlibs/get_driver_path.py b/atest/resources/testlibs/get_driver_path.py new file mode 100644 index 000000000..68042963f --- /dev/null +++ b/atest/resources/testlibs/get_driver_path.py @@ -0,0 +1,47 @@ +""" +>>> from selenium.webdriver.common import driver_finder +>>> drfind = driver_finder.DriverFinder() +>>> from selenium.webdriver.chrome.service import Service +>>> from selenium.webdriver.chrome.options import Options +>>> drfind.get_path(Service(),Options()) + + + def _import_service(self, browser): + browser = browser.replace("headless_", "", 1) + # Throw error is used with remote .. "They cannot be used with a Remote WebDriver session." [ref doc] + service = importlib.import_module(f"selenium.webdriver.{browser}.service") + return service.Service + + def _import_options(self, browser): + browser = browser.replace("headless_", "", 1) + options = importlib.import_module(f"selenium.webdriver.{browser}.options") + return options.Options + +""" +from selenium import webdriver +from selenium.webdriver.common import driver_finder +import importlib + + +def get_driver_path(browser): + browser = browser.lower().replace("headless_", "", 1) + service = importlib.import_module(f"selenium.webdriver.{browser}.service") + options = importlib.import_module(f"selenium.webdriver.{browser}.options") + # finder = driver_finder.DriverFinder() + + # Selenium v4.19.0 and prior + try: + finder = driver_finder.DriverFinder() + func = getattr(finder, 'get_path') + return finder.get_path(service.Service(), options.Options()) + except (AttributeError, TypeError): + pass + + # Selenium V4.20.0 + try: + finder = driver_finder.DriverFinder(service.Service(), options.Options()) + return finder.get_driver_drivepath() + except: + pass + + raise Exception('Unable to determine driver path') \ No newline at end of file diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index 5db13622f..c5bec3c75 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -455,24 +455,6 @@ class SeleniumService: """ """ - # """ executable_path: str = DEFAULT_EXECUTABLE_PATH, - # port: int = 0, - # log_path: typing.Optional[str] = None, - # service_args: typing.Optional[typing.List[str]] = None, - # env: typing.Optional[typing.Mapping[str, str]] = None, - # **kwargs, - # - # executable_path = None, port, service_log_path, service_args, env -# """ -# def create(self, browser, -# executable_path=None, -# port=0, -# service_log_path=None, -# service_args=None, -# env=None, -# start_error_message=None, # chromium, chrome, edge -# quiet=False, reuse_service=False, # safari -# ): def create(self, browser, service): if not service: return None @@ -500,8 +482,7 @@ def _parse(self, service): for item in self._split(service,';'): try: attr, val = self._split(item, '=') - result[attr]=val - # result.append(self._parse_to_tokens(item)) + result[attr]=ast.literal_eval(val) except (ValueError, SyntaxError): raise ValueError(f'Unable to parse service: "{item}"') return result @@ -512,76 +493,6 @@ def _import_service(self, browser): service = importlib.import_module(f"selenium.webdriver.{browser}.service") return service.Service - def _parse_to_tokens(self, item): - result = {} - index, method = self._get_arument_index(item) - if index == -1: - result[item] = [] - return result - if method: - args_as_string = item[index + 1 : -1].strip() - if args_as_string: - args = ast.literal_eval(args_as_string) - else: - args = args_as_string - is_tuple = args_as_string.startswith("(") - else: - args_as_string = item[index + 1 :].strip() - args = ast.literal_eval(args_as_string) - is_tuple = args_as_string.startswith("(") - method_or_attribute = item[:index].strip() - result[method_or_attribute] = self._parse_arguments(args, is_tuple) - return result - - def _old_parse_to_tokens(self, item): - result = {} - index, method = self._get_arument_index(item) - if index == -1: - result[item] = [] - return result - if method: - args_as_string = item[index + 1 : -1].strip() - if args_as_string: - args = ast.literal_eval(args_as_string) - else: - args = args_as_string - is_tuple = args_as_string.startswith("(") - else: - args_as_string = item[index + 1 :].strip() - args = ast.literal_eval(args_as_string) - is_tuple = args_as_string.startswith("(") - method_or_attribute = item[:index].strip() - result[method_or_attribute] = self._parse_arguments(args, is_tuple) - return result - - def _parse_arguments(self, argument, is_tuple=False): - if argument == "": - return [] - if is_tuple: - return [argument] - if not is_tuple and isinstance(argument, tuple): - return list(argument) - return [argument] - - def _get_arument_index(self, item): - if "=" not in item: - return item.find("("), True - if "(" not in item: - return item.find("="), False - index = min(item.find("("), item.find("=")) - return index, item.find("(") == index - - def _oldsplit(self, service): - split_service = [] - start_position = 0 - tokens = generate_tokens(StringIO(service).readline) - for toknum, tokval, tokpos, _, _ in tokens: - if toknum == token.OP and tokval == ";": - split_service.append(service[start_position : tokpos[1]].strip()) - start_position = tokpos[1] + 1 - split_service.append(service[start_position:]) - return split_service - def _split(self, service_or_attr, splittok): split_string = [] start_position = 0 From 64c5283e34a00fe318fa2d4e090ce2baa7f88e3f Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 4 May 2024 22:03:43 -0400 Subject: [PATCH 019/122] Added service argument to many of the browser creation functions All but the Chrome needed to have service class added. Now need to figure out how to handle one using the deprecated service_log_path and executable_path arguments. --- .../keywords/webdrivertools/webdrivertools.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py index c5bec3c75..8f777d160 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/webdrivertools.py @@ -181,7 +181,7 @@ def create_headless_chrome( options = webdriver.ChromeOptions() options.add_argument('--headless=new') return self.create_chrome( - desired_capabilities, remote_url, options, service_log_path, executable_path + desired_capabilities, remote_url, options, service_log_path, executable_path, service ) def _get_executable_path(self, webdriver): @@ -200,6 +200,7 @@ def create_firefox( options=None, service_log_path=None, executable_path="geckodriver", + service=None, ): profile = self._get_ff_profile(ff_profile_dir) if not options: @@ -216,7 +217,8 @@ def create_firefox( if not executable_path: executable_path = self._get_executable_path(webdriver.firefox.service.Service) log_method = self._get_log_method(FirefoxService, service_log_path or self._geckodriver_log) - service = FirefoxService(executable_path=executable_path, **log_method) + if service is None: + service = FirefoxService(executable_path=executable_path, **log_method) return webdriver.Firefox( options=options, service=service, @@ -257,6 +259,7 @@ def create_headless_firefox( options=None, service_log_path=None, executable_path="geckodriver", + service=None, ): if not options: options = webdriver.FirefoxOptions() @@ -268,6 +271,7 @@ def create_headless_firefox( options, service_log_path, executable_path, + service, ) def create_ie( @@ -277,6 +281,7 @@ def create_ie( options=None, service_log_path=None, executable_path="IEDriverServer.exe", + service=None, ): if remote_url: if not options: @@ -285,7 +290,8 @@ def create_ie( if not executable_path: executable_path = self._get_executable_path(webdriver.ie.service.Service) log_method = self._get_log_method(IeService, service_log_path) - service = IeService(executable_path=executable_path, **log_method) + if service is None: + service = IeService(executable_path=executable_path, **log_method) return webdriver.Ie( options=options, service=service, @@ -303,6 +309,7 @@ def create_edge( options=None, service_log_path=None, executable_path="msedgedriver", + service=None, ): if remote_url: if not options: @@ -311,7 +318,8 @@ def create_edge( if not executable_path: executable_path = self._get_executable_path(webdriver.edge.service.Service) log_method = self._get_log_method(EdgeService, service_log_path) - service = EdgeService(executable_path=executable_path, **log_method) + if service is None: + service = EdgeService(executable_path=executable_path, **log_method) return webdriver.Edge( options=options, service=service, @@ -325,6 +333,7 @@ def create_safari( options=None, service_log_path=None, executable_path="/usr/bin/safaridriver", + service=None, ): if remote_url: if not options: @@ -333,7 +342,8 @@ def create_safari( if not executable_path: executable_path = self._get_executable_path(webdriver.Safari) log_method = self._get_log_method(SafariService, service_log_path) - service = SafariService(executable_path=executable_path, **log_method) + if service is None: + service = SafariService(executable_path=executable_path, **log_method) return webdriver.Safari(options=options, service=service) def _remote(self, remote_url, options): From f00df07359e290ebb591e2ebf12267c07f1dfa59 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 4 May 2024 22:39:09 -0400 Subject: [PATCH 020/122] Fixed unit tests and call signature on _make_driver --- .../keywords/test_keyword_arguments_browsermanagement.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utest/test/keywords/test_keyword_arguments_browsermanagement.py b/utest/test/keywords/test_keyword_arguments_browsermanagement.py index 97439ca58..0d730878a 100644 --- a/utest/test/keywords/test_keyword_arguments_browsermanagement.py +++ b/utest/test/keywords/test_keyword_arguments_browsermanagement.py @@ -22,13 +22,13 @@ def test_open_browser(self): remote_url = '"http://localhost:4444/wd/hub"' browser = mock() when(self.brorser)._make_driver( - "firefox", None, None, False, None, None, None + "firefox", None, None, False, None, None, None, None ).thenReturn(browser) alias = self.brorser.open_browser(url) self.assertEqual(alias, None) when(self.brorser)._make_driver( - "firefox", None, None, remote_url, None, None, None + "firefox", None, None, remote_url, None, None, None, None ).thenReturn(browser) alias = self.brorser.open_browser(url, alias="None", remote_url=remote_url) self.assertEqual(alias, None) @@ -47,7 +47,7 @@ def test_same_alias(self): def test_open_browser_no_get(self): browser = mock() when(self.brorser)._make_driver( - "firefox", None, None, False, None, None, None + "firefox", None, None, False, None, None, None, None ).thenReturn(browser) self.brorser.open_browser() verify(browser, times=0).get(ANY) From ea75a82f118fb93099701627bbcc87e12bf3a93f Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Sun, 5 May 2024 22:58:02 +0300 Subject: [PATCH 021/122] Make test run on Mac Do not hard code port, it used in Mac (sometimes) --- .../1-plugin/plugin_does_not_exist.robot | 2 +- .../1-plugin/plugin_open_browser.robot | 2 +- .../event_firing_webdriver.robot | 4 ++-- .../resource_event_firing_webdriver.robot | 11 ++------- .../keywords/click_element_modifier.robot | 6 ++++- atest/acceptance/keywords/elements.robot | 8 +++---- atest/acceptance/keywords/location.robot | 24 ++++++++++--------- atest/acceptance/resource.robot | 10 +------- atest/acceptance/variables.robot | 9 +++++++ .../html/javascript/wait_location.html | 3 ++- atest/resources/testserver/testserver.py | 16 ++++++++----- atest/run.py | 21 ++++++++++------ 12 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 atest/acceptance/variables.robot diff --git a/atest/acceptance/1-plugin/plugin_does_not_exist.robot b/atest/acceptance/1-plugin/plugin_does_not_exist.robot index 503c32f71..e97d25f21 100644 --- a/atest/acceptance/1-plugin/plugin_does_not_exist.robot +++ b/atest/acceptance/1-plugin/plugin_does_not_exist.robot @@ -1,5 +1,5 @@ *** Variables *** -${SERVER}= localhost:7000 +${SERVER}= localhost:${PORT} ${BROWSER}= firefox ${REMOTE_URL}= ${NONE} ${ROOT}= http://${SERVER}/html diff --git a/atest/acceptance/1-plugin/plugin_open_browser.robot b/atest/acceptance/1-plugin/plugin_open_browser.robot index 41247443b..9d391d956 100644 --- a/atest/acceptance/1-plugin/plugin_open_browser.robot +++ b/atest/acceptance/1-plugin/plugin_open_browser.robot @@ -2,7 +2,7 @@ Library SeleniumLibrary plugins=${CURDIR}/OpenBrowserExample.py *** Variables *** -${SERVER}= localhost:7000 +${SERVER}= localhost:${PORT} ${ROOT}= http://${SERVER}/html &{EXTRA DICTIONARY} extra=dictionary key=value diff --git a/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot b/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot index b31c8fa91..2ee7904a1 100644 --- a/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot +++ b/atest/acceptance/2-event_firing_webdriver/event_firing_webdriver.robot @@ -1,7 +1,7 @@ *** Settings *** Library SeleniumLibrary event_firing_webdriver=${CURDIR}/../../resources/testlibs/MyListener.py Resource resource_event_firing_webdriver.robot -Suite Setup Open Browser ${FRONT PAGE} ${BROWSER} alias=event_firing_webdriver +Suite Setup Open Browser ${FRONT_PAGE} ${BROWSER} alias=event_firing_webdriver ... remote_url=${REMOTE_URL} Suite Teardown Close All Browsers @@ -14,7 +14,7 @@ Open Browser To Start Page [Documentation] ... LOG 1:31 DEBUG Wrapping driver to event_firing_webdriver. ... LOG 1:33 INFO Got driver also from SeleniumLibrary. - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} + Open Browser ${FRONT_PAGE} ${BROWSER} remote_url=${REMOTE_URL} Event Firing Webdriver Go To (WebDriver) [Tags] NoGrid diff --git a/atest/acceptance/2-event_firing_webdriver/resource_event_firing_webdriver.robot b/atest/acceptance/2-event_firing_webdriver/resource_event_firing_webdriver.robot index a224bc9bd..27e92aab7 100644 --- a/atest/acceptance/2-event_firing_webdriver/resource_event_firing_webdriver.robot +++ b/atest/acceptance/2-event_firing_webdriver/resource_event_firing_webdriver.robot @@ -1,14 +1,7 @@ -*** Variables *** -${SERVER}= localhost:7000 -${BROWSER}= Chrome -${REMOTE_URL}= ${NONE} -${DESIRED_CAPABILITIES}= ${NONE} -${ROOT}= http://${SERVER}/html -${FRONT_PAGE}= ${ROOT}/ +*** Settings *** +Resource ../variables.robot *** Keywords *** - Go To Page "${relative url}" [Documentation] Goes to page Go To ${ROOT}/${relative url} - diff --git a/atest/acceptance/keywords/click_element_modifier.robot b/atest/acceptance/keywords/click_element_modifier.robot index f83a0317d..f3b9ac5ce 100644 --- a/atest/acceptance/keywords/click_element_modifier.robot +++ b/atest/acceptance/keywords/click_element_modifier.robot @@ -5,19 +5,23 @@ Resource ../resource.robot *** Test Cases *** Click Element Modifier CTRL + [Tags] SKIP_ON_MAC Click Element Button modifier=CTRL Element Text Should Be output CTRL click Click Link Modifier CTRL + [Tags] SKIP_ON_MAC Click Link link text modifier=CTRL Element Text Should Be output CTRL click [Teardown] Close Popup Window Click Button Modifier CTRL + [Tags] SKIP_ON_MAC Click Button Click me! modifier=CTRL Element Text Should Be output CTRL click Click Image Modifier CTRL + [Tags] SKIP_ON_MAC Click Image robot modifier=CTRL Element Text Should Be output CTRL click @@ -46,7 +50,7 @@ Click Element Action Chain and modifier [Documentation] LOG 1:1 INFO Clicking element 'Button' with CTRL. Click Element Button modifier=CTRL action_chain=True Element Text Should Be output CTRL click - + *** Keywords *** Initialize Page Reload Page diff --git a/atest/acceptance/keywords/elements.robot b/atest/acceptance/keywords/elements.robot index fa053b0ff..a93ccd219 100644 --- a/atest/acceptance/keywords/elements.robot +++ b/atest/acceptance/keywords/elements.robot @@ -90,7 +90,7 @@ Get DOM Attribute Should Be Equal ${id} some_id # Test custom attribute ${existing_custom_attr}= Get DOM Attribute id:emptyDiv data-id - Should Be Equal ${existing_custom_attr} my_id + Should Be Equal ${existing_custom_attr} my_id ${doesnotexist_custom_attr}= Get DOM Attribute id:emptyDiv data-doesnotexist Should Be Equal ${doesnotexist_custom_attr} ${None} # Get non existing DOM Attribute @@ -156,7 +156,7 @@ Get Element Attribute And Element Attribute Value Should Be Should have same res Element Attribute Value Should Be css=#second_div class ${attribute_value} Get Element Attribute Value Should Be Should Be Succesfull with non-ascii characters - Element Attribute Value Should Be link=Link with Unicode äöüÄÖÜß href http://localhost:7000/html/index.html + Element Attribute Value Should Be link=Link with Unicode äöüÄÖÜß href ${FRONT_PAGE}index.html Get Element Attribute Value Should Be Should Be Succesfull error and error messages Run Keyword And Expect Error @@ -169,8 +169,8 @@ Get Element Attribute Value Should Be Should Be Succesfull error and error messa ... Element with locator 'id=non_existing' not found. ... Element Attribute Value Should Be id=non_existing href http://non_existing.com Run Keyword And Expect Error - ... Element 'link=Target opens in new window' attribute should have value 'http://localhost:7000/html/indéx.html' (str) but its value was 'http://localhost:7000/html/index.html' (str). - ... Element Attribute Value Should Be link=Target opens in new window href http://localhost:7000/html/indéx.html + ... Element 'link=Target opens in new window' attribute should have value '${FRONT_PAGE}indéx.html' (str) but its value was '${FRONT_PAGE}index.html' (str). + ... Element Attribute Value Should Be link=Target opens in new window href ${FRONT_PAGE}indéx.html Get Horizontal Position ${pos}= Get Horizontal Position link=Link diff --git a/atest/acceptance/keywords/location.robot b/atest/acceptance/keywords/location.robot index 19d9ac1d8..d96fdb3aa 100644 --- a/atest/acceptance/keywords/location.robot +++ b/atest/acceptance/keywords/location.robot @@ -44,12 +44,13 @@ Wait Until Location Contains At The End Wait Until Location Contains In The Middle [Setup] Go To Page "javascript/wait_location.html" Click Element button - Wait Until Location Contains 7000 + Wait Until Location Contains ${PORT} Wait Until Location Contains As Number [Setup] Go To Page "javascript/wait_location.html" Click Element button - Wait Until Location Contains ${7000} + ${number} Convert To Integer ${PORT} + Wait Until Location Contains ${number} Wait Until Location Contains Fails [Setup] Go To Page "javascript/wait_location.html" @@ -70,7 +71,7 @@ Wait Until Location Contains Fails With Timeout Wait Until Location Is [Setup] Go To Page "javascript/wait_location.html" Click Element button - Wait Until Location Is http://localhost:7000/html/ + Wait Until Location Is ${FRONT_PAGE} Wait Until Location Is Fails [Setup] Go To Page "javascript/wait_location.html" @@ -91,14 +92,14 @@ Wait Until Location Is Fails With Timeout Wait Until Location Is Not [Setup] Go To Page "javascript/wait_location.html" Click Element button - Wait Until Location Is Not http://localhost:7000/html/javascript/wait_location.html + Wait Until Location Is Not ${FRONT_PAGE}javascript/wait_location.html Wait Until Location Is Not Fail [Setup] Go To Page "javascript/wait_location.html" ${orig_timeout}= Set Selenium Timeout 2 s Run Keyword And Expect Error - ... Location is 'http://localhost:7000/html/javascript/wait_location.html' in 2 seconds. - ... Wait Until Location Is Not http://localhost:7000/html/javascript/wait_location.html + ... Location is '${FRONT_PAGE}javascript/wait_location.html' in 2 seconds. + ... Wait Until Location Is Not ${FRONT_PAGE}javascript/wait_location.html Set Selenium Timeout ${orig_timeout} Wait Until Location Is Not Fails With Timeout @@ -106,8 +107,8 @@ Wait Until Location Is Not Fails With Timeout ${orig_timeout}= Set Selenium Timeout 2 s Click Element button Run Keyword And Expect Error - ... Location is 'http://localhost:7000/html/javascript/wait_location.html' in 750 milliseconds. - ... Wait Until Location Is Not http://localhost:7000/html/javascript/wait_location.html timeout=750ms + ... Location is '${FRONT_PAGE}javascript/wait_location.html' in 750 milliseconds. + ... Wait Until Location Is Not ${FRONT_PAGE}javascript/wait_location.html timeout=750ms Set Selenium Timeout ${orig_timeout} Wait Until Location Is Not Fails With Message @@ -116,7 +117,7 @@ Wait Until Location Is Not Fails With Message ${orig_timeout}= Set Selenium Timeout 2 s Run Keyword And Expect Error ... my_message - ... Wait Until Location Is Not http://localhost:7000/html/javascript/wait_location.html message=my_message + ... Wait Until Location Is Not ${FRONT_PAGE}javascript/wait_location.html message=my_message Set Selenium Timeout ${orig_timeout} Wait Until Location Does Not Contain @@ -143,9 +144,10 @@ Wait Until Location Does Not Contain Fail In The Middle Wait Until Location Does Not Contain Fail As Number [Setup] Go To Page "javascript/wait_location.html" ${orig_timeout}= Set Selenium Timeout 2 s + ${number} Convert To Integer ${PORT} run keyword and expect error - ... Location did contain '${7000}' in 2 seconds. - ... Wait Until Location Does Not Contain ${7000} + ... Location did contain '${number}' in 2 seconds. + ... Wait Until Location Does Not Contain ${number} Set Selenium Timeout ${orig_timeout} Wait Until Location Does Not Contain Fail At The End diff --git a/atest/acceptance/resource.robot b/atest/acceptance/resource.robot index 4bcaf38eb..7b429d0ff 100644 --- a/atest/acceptance/resource.robot +++ b/atest/acceptance/resource.robot @@ -3,15 +3,7 @@ Library SeleniumLibrary run_on_failure=Nothing implicit_wait=0.2 Library Collections Library OperatingSystem Library DateTime - -*** Variables *** -${SERVER}= localhost:7000 -${BROWSER}= firefox -${REMOTE_URL}= ${NONE} -${DESIRED_CAPABILITIES}= ${NONE} -${ROOT}= http://${SERVER}/html -${FRONT_PAGE}= ${ROOT}/ -${SPEED}= 0 +Resource variables.robot *** Keywords *** Open Browser To Start Page diff --git a/atest/acceptance/variables.robot b/atest/acceptance/variables.robot new file mode 100644 index 000000000..b613ccb5a --- /dev/null +++ b/atest/acceptance/variables.robot @@ -0,0 +1,9 @@ +*** Variables *** +${PORT}= 7000 +${SERVER}= localhost:${PORT} +${BROWSER}= firefox +${REMOTE_URL}= ${NONE} +${DESIRED_CAPABILITIES}= ${NONE} +${ROOT}= http://${SERVER}/html +${FRONT_PAGE}= ${ROOT}/ +${SPEED}= 0 \ No newline at end of file diff --git a/atest/resources/html/javascript/wait_location.html b/atest/resources/html/javascript/wait_location.html index c4e96c147..68bc0478e 100644 --- a/atest/resources/html/javascript/wait_location.html +++ b/atest/resources/html/javascript/wait_location.html @@ -5,7 +5,8 @@ setTimeout('doLocation()', 1000) } function doLocation() { - self.location = 'http://localhost:7000/html/' + var base_url = window.location.origin; + self.location = `${base_url}/html/` } diff --git a/atest/resources/testserver/testserver.py b/atest/resources/testserver/testserver.py index 565c6506f..b3e582e88 100644 --- a/atest/resources/testserver/testserver.py +++ b/atest/resources/testserver/testserver.py @@ -1,6 +1,7 @@ # Initially based on: # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/336012 +import argparse import os import sys @@ -40,11 +41,14 @@ def start_server(path, port=7000): if __name__ == "__main__": - if len(sys.argv) != 2 or sys.argv[1] not in ["start", "stop"]: - print("usage: %s start|stop" % sys.argv[0]) - sys.exit(1) - if sys.argv[1] == "start": + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=["start", "stop"]) + parser.add_argument("--port", default=7000, type=int) + args = parser.parse_args() + port = args.port + command = args.command + if command == "start": path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - start_server(path) + start_server(path, port) else: - stop_server() + stop_server(port) diff --git a/atest/run.py b/atest/run.py index 2c86cf994..3e7960a15 100755 --- a/atest/run.py +++ b/atest/run.py @@ -53,6 +53,7 @@ from robot import rebot_cli from robot import __version__ as robot_version from selenium import __version__ as selenium_version +from selenium.webdriver.common.utils import free_port from robot.utils import is_truthy try: @@ -107,10 +108,12 @@ def acceptance_tests( if os.path.exists(RESULTS_DIR): shutil.rmtree(RESULTS_DIR) os.mkdir(RESULTS_DIR) + port = free_port() + print(f"Using port: {port}") if grid: hub, node = start_grid() - with http_server(interpreter): - execute_tests(interpreter, browser, rf_options, grid, event_firing) + with http_server(interpreter, port): + execute_tests(interpreter, browser, rf_options, grid, event_firing, port) failures = process_output(browser) if failures: print( @@ -178,23 +181,23 @@ def _grid_status(status=False, role="hub"): @contextmanager -def http_server(interpreter): +def http_server(interpreter, port:int): serverlog = open(os.path.join(RESULTS_DIR, "serverlog.txt"), "w") interpreter = "python" if not interpreter else interpreter process = subprocess.Popen( - [interpreter, HTTP_SERVER_FILE, "start"], + [interpreter, HTTP_SERVER_FILE, "start", "--port", str(port)], stdout=serverlog, stderr=subprocess.STDOUT, ) try: yield finally: - subprocess.call([interpreter, HTTP_SERVER_FILE, "stop"]) + subprocess.call([interpreter, HTTP_SERVER_FILE, "stop", "--port", str(port)]) process.wait() serverlog.close() -def execute_tests(interpreter, browser, rf_options, grid, event_firing): +def execute_tests(interpreter, browser, rf_options, grid, event_firing, port): options = [] if grid: runner = interpreter.split() + [ @@ -204,7 +207,11 @@ def execute_tests(interpreter, browser, rf_options, grid, event_firing): "2", ] else: - runner = interpreter.split() + ["-m", "robot.run"] + runner = interpreter.split() + ["-m", "robot.run", "--variable", f"PORT:{port}"] + if platform.system() == "Darwin": + runner.append("--exclude") + runner.append("SKIP_ON_MAC") + options.extend([opt.format(browser=browser) for opt in ROBOT_OPTIONS]) if rf_options: options += rf_options From b6170c142a1b17603c55b6922aec5feae1013ad4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 5 May 2024 18:30:30 -0400 Subject: [PATCH 022/122] Removed initial browser_service.robot as now have tests within multiple_browsers_service.robot --- atest/acceptance/browser_service.robot | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 atest/acceptance/browser_service.robot diff --git a/atest/acceptance/browser_service.robot b/atest/acceptance/browser_service.robot deleted file mode 100644 index bdce3f4e8..000000000 --- a/atest/acceptance/browser_service.robot +++ /dev/null @@ -1,9 +0,0 @@ -*** Settings *** -Suite Teardown Close All Browsers -Resource resource.robot -Documentation These tests check the service argument of Open Browser. - -*** Test Cases *** -Browser With Selenium Service As String - Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} - ... service=port=1234; executable_path='/path/to/driver/executable' From 8d8fec09626c00748f51a33f2afc29761e31038b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 5 May 2024 19:33:06 -0400 Subject: [PATCH 023/122] Updated log check correcting for deprecated warning --- atest/acceptance/multiple_browsers_service_log_path.robot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atest/acceptance/multiple_browsers_service_log_path.robot b/atest/acceptance/multiple_browsers_service_log_path.robot index ee22a3b25..364713b70 100644 --- a/atest/acceptance/multiple_browsers_service_log_path.robot +++ b/atest/acceptance/multiple_browsers_service_log_path.robot @@ -5,7 +5,7 @@ Resource resource.robot *** Test Cases *** First Browser With Service Log Path [Documentation] - ... LOG 1:2 INFO STARTS: Browser driver log file created to: + ... LOG 1:3 INFO STARTS: Browser driver log file created to: [Setup] OperatingSystem.Remove Files ${OUTPUT DIR}/${BROWSER}.log Open Browser ${FRONT PAGE} ${BROWSER} service_log_path=${OUTPUT DIR}/${BROWSER}.log OperatingSystem.List Directories In Directory ${OUTPUT DIR}/ From 1415f1236959e2ca38c13b78a6350ad4158f91f0 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 5 May 2024 19:38:56 -0400 Subject: [PATCH 024/122] Added service argument to create browser methods --- atest/acceptance/1-plugin/OpenBrowserExample.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/atest/acceptance/1-plugin/OpenBrowserExample.py b/atest/acceptance/1-plugin/OpenBrowserExample.py index b279ab133..2cb006f24 100644 --- a/atest/acceptance/1-plugin/OpenBrowserExample.py +++ b/atest/acceptance/1-plugin/OpenBrowserExample.py @@ -24,6 +24,7 @@ def open_browser( service_log_path=None, extra_dictionary=None, executable_path=None, + service=None, ): self._new_creator.extra_dictionary = extra_dictionary browser_manager = BrowserManagementKeywords(self.ctx) @@ -37,7 +38,8 @@ def open_browser( ff_profile_dir=ff_profile_dir, options=options, service_log_path=service_log_path, - executable_path=None, + executable_path=executable_path, + service=service, ) def _make_driver( @@ -49,6 +51,7 @@ def _make_driver( options=None, service_log_path=None, executable_path=None, + service=None, ): driver = self._new_creator.create_driver( browser=browser, @@ -58,6 +61,7 @@ def _make_driver( options=options, service_log_path=service_log_path, executable_path=executable_path, + service=None, ) driver.set_script_timeout(self.ctx.timeout) driver.implicitly_wait(self.ctx.implicit_wait) @@ -76,6 +80,7 @@ def create_driver( options=None, service_log_path=None, executable_path=None, + service=None, ): self.browser_names["seleniumwire"] = "seleniumwire" browser = self._normalise_browser_name(browser) @@ -83,6 +88,7 @@ def create_driver( desired_capabilities = self._parse_capabilities(desired_capabilities, browser) service_log_path = self._get_log_path(service_log_path) options = self.selenium_options.create(self.browser_names.get(browser), options) + service = self.selenium_service.create(self.browser_names.get(browser), service) if service_log_path: logger.info("Browser driver log file created to: %s" % service_log_path) self._create_directory(service_log_path) @@ -96,6 +102,7 @@ def create_driver( profile_dir, options=options, service_log_path=service_log_path, + service=service, ) if creation_method == self.create_seleniumwire: return creation_method( @@ -103,16 +110,18 @@ def create_driver( remote_url, options=options, service_log_path=service_log_path, + service=service, ) return creation_method( desired_capabilities, remote_url, options=options, service_log_path=service_log_path, + service=service, ) def create_seleniumwire( - self, desired_capabilities, remote_url, options=None, service_log_path=None + self, desired_capabilities, remote_url, options=None, service_log_path=None, service=None, ): logger.info(self.extra_dictionary) return webdriver.Chrome() From e34282e582b6572d65baf33b63eddc5c64cda4c8 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Tue, 7 May 2024 22:07:20 -0400 Subject: [PATCH 025/122] Added more unit tests for Service string --- .../keywords/webdrivertools/__init__.py | 1 + ..._service_parser.test_importer.approved.txt | 9 ++ ...ser.test_parse_service_string.approved.txt | 9 ++ ...t_parse_service_string_errors.approved.txt | 8 ++ ...ce_parser.test_service_create.approved.txt | 5 + ...e_parser.test_split_attribute.approved.txt | 5 + ...ice_parser.test_split_service.approved.txt | 6 + .../keywords/test_selenium_service_parser.py | 124 ++++++++++++++++++ 8 files changed, 167 insertions(+) create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_importer.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_service_create.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_split_attribute.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_split_service.approved.txt create mode 100644 utest/test/keywords/test_selenium_service_parser.py diff --git a/src/SeleniumLibrary/keywords/webdrivertools/__init__.py b/src/SeleniumLibrary/keywords/webdrivertools/__init__.py index e95ee378f..9d55ab897 100644 --- a/src/SeleniumLibrary/keywords/webdrivertools/__init__.py +++ b/src/SeleniumLibrary/keywords/webdrivertools/__init__.py @@ -17,4 +17,5 @@ from .webdrivertools import WebDriverCreator # noqa from .webdrivertools import WebDriverCache # noqa from .webdrivertools import SeleniumOptions # noqa +from .webdrivertools import SeleniumService # noqa from .sl_file_detector import SelLibLocalFileDetector # noqa diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_importer.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_importer.approved.txt new file mode 100644 index 000000000..f71cb14d5 --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_importer.approved.txt @@ -0,0 +1,9 @@ +Selenium service import + +0) +1) +2) +3) +4) +5) +6) diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt new file mode 100644 index 000000000..82bba3327 --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt @@ -0,0 +1,9 @@ +Selenium service string to dict + +0) {'attribute': 'arg1'} +1) {'attribute': True} +2) {'attribute': True} +3) {'attribute': 'arg4'} +4) {'attribute': 'C:\\path\to\\profile'} +5) {'attribute': 'C:\\path\\to\\profile'} +6) {'attribute': None} diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors.approved.txt new file mode 100644 index 000000000..e7bf1c5ad --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors.approved.txt @@ -0,0 +1,8 @@ +Selenium service string errors + +0) attribute=arg1 Unable to parse service: "attribute=arg1" +1) attribute='arg1 Unable to parse service: "attribute='arg1" +2) attribute=['arg1' ('EOF in multi-line statement', (2, 0)) +3) attribute=['arg1';'arg2'] ('EOF in multi-line statement', (2, 0)) +4) attribute['arg1'] Unable to parse service: "attribute['arg1']" +5) attribute=['arg1'] attribute=['arg2'] Unable to parse service: "attribute=['arg1'] attribute=['arg2']" diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_service_create.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_service_create.approved.txt new file mode 100644 index 000000000..678b8a6ba --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_service_create.approved.txt @@ -0,0 +1,5 @@ +Selenium service + +0) ['--log-level=DEBUG'] +1) ['--append-log', '--readable-timestamp'] +2) ['--disable-build-check'] diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_attribute.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_attribute.approved.txt new file mode 100644 index 000000000..f0bb0c6cb --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_attribute.approved.txt @@ -0,0 +1,5 @@ +Selenium service attribute string splitting + +0) ['attribute', "'arg1'"] +1) ['attribute', "['arg1','arg2']"] +2) ['attribute', " [ 'arg1' , 'arg2' ]"] diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_service.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_service.approved.txt new file mode 100644 index 000000000..8f2026ad8 --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_split_service.approved.txt @@ -0,0 +1,6 @@ +Selenium service string splitting + +0) ["attribute='arg1'"] +1) ["attribute='arg1'", "attribute='arg2'"] +2) ["attribute=['arg1','arg2']", "attribute='arg3'"] +3) ["attribute = 'arg1'", " attribute = 'arg2' "] diff --git a/utest/test/keywords/test_selenium_service_parser.py b/utest/test/keywords/test_selenium_service_parser.py new file mode 100644 index 000000000..1649234a0 --- /dev/null +++ b/utest/test/keywords/test_selenium_service_parser.py @@ -0,0 +1,124 @@ +import os +import unittest + +import pytest +from approvaltests.approvals import verify_all +from approvaltests.reporters.generic_diff_reporter_factory import ( + GenericDiffReporterFactory, +) +from mockito import mock, when, unstub, ANY +from robot.utils import WINDOWS +from selenium import webdriver + +from SeleniumLibrary.keywords.webdrivertools import SeleniumService, WebDriverCreator + + +@pytest.fixture(scope="module") +def service(): + return SeleniumService() + +@pytest.fixture(scope="module") +def reporter(): + path = os.path.dirname(__file__) + reporter_json = os.path.abspath( + os.path.join(path, "..", "approvals_reporters.json") + ) + factory = GenericDiffReporterFactory() + factory.load(reporter_json) + return factory.get_first_working() + + +def teardown_function(): + unstub() + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_parse_service_string(service, reporter): + results = [] + results.append(service._parse('attribute="arg1"')) + results.append(service._parse(" attribute = True ")) + results.append(service._parse('attribute="arg1";attribute=True')) + results.append(service._parse('attribute=["arg1","arg2","arg3"] ; attribute=True ; attribute="arg4"')) + results.append( + service._parse( + 'attribute="C:\\\\path\\to\\\\profile"' + ) + ) + results.append( + service._parse( + r'attribute="arg1"; attribute="C:\\path\\to\\profile"' + ) + ) + results.append(service._parse("attribute=None")) + verify_all("Selenium service string to dict", results, reporter=reporter) + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_parse_service_string_errors(service, reporter): + results = [] + results.append(error_formatter(service._parse, "attribute=arg1", True)) + results.append(error_formatter(service._parse, "attribute='arg1", True)) + results.append(error_formatter(service._parse, "attribute=['arg1'", True)) + results.append(error_formatter(service._parse, "attribute=['arg1';'arg2']", True)) + results.append(error_formatter(service._parse, "attribute['arg1']", True)) + results.append(error_formatter(service._parse, "attribute=['arg1'] attribute=['arg2']", True)) + verify_all("Selenium service string errors", results, reporter=reporter) + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_split_service(service, reporter): + results = [] + results.append(service._split("attribute='arg1'", ';')) + results.append(service._split("attribute='arg1';attribute='arg2'", ';')) + results.append(service._split("attribute=['arg1','arg2'];attribute='arg3'", ';')) + results.append(service._split(" attribute = 'arg1' ; attribute = 'arg2' ", ';')) + verify_all("Selenium service string splitting", results, reporter=reporter) + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_split_attribute(service, reporter): + results = [] + results.append(service._split("attribute='arg1'", '=')) + results.append(service._split("attribute=['arg1','arg2']", '=')) + results.append(service._split(" attribute = [ 'arg1' , 'arg2' ]", '=')) + verify_all("Selenium service attribute string splitting", results, reporter=reporter) + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_service_create(service, reporter): + results = [] + service_str = "service_args=['--log-level=DEBUG']" + brwsr_service = service.create("chrome", service_str) + results.append(brwsr_service.service_args) + + service_str = f"{service_str};service_args=['--append-log', '--readable-timestamp']" + brwsr_service = service.create("chrome", service_str) + results.append(brwsr_service.service_args) + + service_str = f"{service_str};service_args=['--disable-build-check']" + brwsr_service = service.create("chrome", service_str) + results.append(brwsr_service.service_args) + + verify_all("Selenium service", results, reporter=reporter) + + +@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +def test_importer(service, reporter): + results = [] + results.append(service._import_service("firefox")) + results.append(service._import_service("headless_firefox")) + results.append(service._import_service("chrome")) + results.append(service._import_service("headless_chrome")) + results.append(service._import_service("ie")) + results.append(service._import_service("edge")) + results.append(service._import_service("safari")) + verify_all("Selenium service import", results, reporter=reporter) + + +def error_formatter(method, arg, full=False): + try: + return method(arg) + except Exception as error: + if full: + return f"{arg} {error}" + return "{} {}".format(arg, error.__str__()[:15]) \ No newline at end of file From 81f7d0cd6d83e5c3366fa8c8d9dfee76d472f972 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Wed, 8 May 2024 22:14:25 -0400 Subject: [PATCH 026/122] Initial reworking of the `Open Browser` keyword documentation A major overhaul of the keyword documentation for `Open Browser`. - Added some deprecation warnings for ``service_log_path`` and ``executable_path`` options. Also noted that the already deprecated ``desired_capabilities`` will be removed in the next release. - Removed note about partial support for options as it is now full support. - Moved majority of options description up to the main library documentation. - Reshuffled examples and put them closery to the arguments they demonstarte. - Removed many of the "this feature was added in version x" messages as they are very old changes. --- src/SeleniumLibrary/__init__.py | 82 ++++++++++ .../keywords/browsermanagement.py | 144 +++++------------- 2 files changed, 117 insertions(+), 109 deletions(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index fe550aec5..b10273dfb 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -322,6 +322,88 @@ class SeleniumLibrary(DynamicCore): https://robocon.io/, https://github.com/robotframework/' and 'https://github.com/. + = Browser and Driver options = + + This section talks about how to configure either the browser or + the driver using the options ans service arguments of the `Open + Browser` keyword. + + == Configuring the browser using the Selenium Options == + + As noted within the keyword documentation for `Open Browser`, its + ``options`` argument accepts Selenium options in two different + formats: as a string and as Python object which is an instance of + the Selenium options class. + + === Options string format === + + The string format allows defining Selenium options methods + or attributes and their arguments in Robot Framework test data. + The method and attributes names are case and space sensitive and + must match to the Selenium options methods and attributes names. + When defining a method, it must be defined in a similar way as in + python: method name, opening parenthesis, zero to many arguments + and closing parenthesis. If there is a need to define multiple + arguments for a single method, arguments must be separated with + comma, just like in Python. Example: `add_argument("--headless")` + or `add_experimental_option("key", "value")`. Attributes are + defined in a similar way as in Python: attribute name, equal sign, + and attribute value. Example, `headless=True`. Multiple methods + and attributes must be separated by a semicolon. Example: + `add_argument("--headless");add_argument("--start-maximized")`. + + Arguments allow defining Python data types and arguments are + evaluated by using Python + [https://docs.python.org/3/library/ast.html#ast.literal_eval|ast.literal_eval]. + Strings must be quoted with single or double quotes, example "value" + or 'value'. It is also possible to define other Python builtin + data types, example `True` or `None`, by not using quotes + around the arguments. + + The string format is space friendly. Usually, spaces do not alter + the defining methods or attributes. There are two exceptions. + In some Robot Framework test data formats, two or more spaces are + considered as cell separator and instead of defining a single + argument, two or more arguments may be defined. Spaces in string + arguments are not removed and are left as is. Example + `add_argument ( "--headless" )` is same as + `add_argument("--headless")`. But `add_argument(" --headless ")` is + not same same as `add_argument ( "--headless" )`, because + spaces inside of quotes are not removed. Please note that if + options string contains backslash, example a Windows OS path, + the backslash needs escaping both in Robot Framework data and + in Python side. This means single backslash must be writen using + four backslash characters. Example, Windows path: + "C:\\path\\to\\profile" must be written as + "C:\\\\\\\\path\\\\\\to\\\\\\\\profile". Another way to write + backslash is use Python + [https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals|raw strings] + and example write: r"C:\\\\path\\\\to\\\\profile". + + === Selenium Options as Python class === + + As last format, ``options`` argument also supports receiving + the Selenium options as Python class instance. In this case, the + instance is used as-is and the SeleniumLibrary will not convert + the instance to other formats. + For example, if the following code return value is saved to + `${options}` variable in the Robot Framework data: + | options = webdriver.ChromeOptions() + | options.add_argument('--disable-dev-shm-usage') + | return options + + Then the `${options}` variable can be used as an argument to + ``options``. + + Example the ``options`` argument can be used to launch Chomium-based + applications which utilize the + [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] + . To lauch Chomium-based application, use ``options`` to define + `binary_location` attribute and use `add_argument` method to define + `remote-debugging-port` port for the application. Once the browser + is opened, the test can interact with the embedded web-content of + the system under test. + = Timeouts, waits, and delays = This section discusses different ways how to wait for elements to diff --git a/src/SeleniumLibrary/keywords/browsermanagement.py b/src/SeleniumLibrary/keywords/browsermanagement.py index 57c15087b..a7825e44a 100644 --- a/src/SeleniumLibrary/keywords/browsermanagement.py +++ b/src/SeleniumLibrary/keywords/browsermanagement.py @@ -88,13 +88,18 @@ def open_browser( To be able to actually use one of these browsers, you need to have a matching Selenium browser driver available. See the [https://github.com/robotframework/SeleniumLibrary#browser-drivers| - project documentation] for more details. Headless Firefox and - Headless Chrome are new additions in SeleniumLibrary 3.1.0 - and require Selenium 3.8.0 or newer. + project documentation] for more details. After opening the browser, it is possible to use optional ``url`` to navigate the browser to the desired address. + Examples: + | `Open Browser` | http://example.com | Chrome | | + | `Open Browser` | http://example.com | Firefox | alias=Firefox | + | `Open Browser` | http://example.com | Edge | remote_url=http://127.0.0.1:4444/wd/hub | + | `Open Browser` | about:blank | | | + | `Open Browser` | browser=Chrome | | | + Optional ``alias`` is an alias given for this browser instance and it can be used for switching between browsers. When same ``alias`` is given with two `Open Browser` keywords, the first keyword will @@ -108,18 +113,26 @@ def open_browser( browsers are opened, and reset back to 1 when `Close All Browsers` is called. See `Switch Browser` for more information and examples. + Alias examples: + | ${1_index} = | `Open Browser` | http://example.com | Chrome | alias=Chrome | # Opens new browser because alias is new. | + | ${2_index} = | `Open Browser` | http://example.com | Firefox | | # Opens new browser because alias is not defined. | + | ${3_index} = | `Open Browser` | http://example.com | Chrome | alias=Chrome | # Switches to the browser with Chrome alias. | + | ${4_index} = | `Open Browser` | http://example.com | Chrome | alias=${1_index} | # Switches to the browser with Chrome alias. | + | Should Be Equal | ${1_index} | ${3_index} | | | | + | Should Be Equal | ${1_index} | ${4_index} | | | | + | Should Be Equal | ${2_index} | ${2} | | | | + Optional ``remote_url`` is the URL for a [https://github.com/SeleniumHQ/selenium/wiki/Grid2|Selenium Grid]. - Optional ``desired_capabilities`` is deprecated and will be ignored. Capabilities of each - individual browser is now done through options or services. Please refer to those arguments + Optional ``desired_capabilities`` is deprecated and will be removed + in the next release. Capabilities of each individual browser is now + done through options or services. Please refer to those arguments for configuring specific browsers. Optional ``ff_profile_dir`` is the path to the Firefox profile directory if you wish to overwrite the default profile Selenium - uses. Notice that prior to SeleniumLibrary 3.0, the library - contained its own profile that was used by default. The - ``ff_profile_dir`` can also be an instance of the + uses. The ``ff_profile_dir`` can also be an instance of the [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_firefox/selenium.webdriver.firefox.firefox_profile.html|selenium.webdriver.FirefoxProfile] . As a third option, it is possible to use `FirefoxProfile` methods and attributes to define the profile using methods and attributes @@ -128,86 +141,28 @@ def open_browser( profile settings. See ``options`` argument documentation in below how to handle backslash escaping. + Example for FirefoxProfile + | `Open Browser` | http://example.com | Firefox | ff_profile_dir=/path/to/profile | # Using profile from disk. | + | `Open Browser` | http://example.com | Firefox | ff_profile_dir=${FirefoxProfile_instance} | # Using instance of FirefoxProfile. | + | `Open Browser` | http://example.com | Firefox | ff_profile_dir=set_preference("key", "value");set_preference("other", "setting") | # Defining profile using FirefoxProfile mehtods. | + Optional ``options`` argument allows defining browser specific Selenium options. Example for Chrome, the ``options`` argument allows defining the following [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.options.html#selenium.webdriver.chrome.options.Options|methods and attributes] and for Firefox these [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_firefox/selenium.webdriver.firefox.options.html?highlight=firefox#selenium.webdriver.firefox.options.Options|methods and attributes] - are available. Please note that not all browsers, supported by the - SeleniumLibrary, have Selenium options available. Therefore please - consult the Selenium documentation which browsers do support - the Selenium options. Selenium options are also supported, when ``remote_url`` + are available. Selenium options are also supported, when ``remote_url`` argument is used. The SeleniumLibrary ``options`` argument accepts Selenium options in two different formats: as a string and as Python object which is an instance of the Selenium options class. - The string format allows defining Selenium options methods - or attributes and their arguments in Robot Framework test data. - The method and attributes names are case and space sensitive and - must match to the Selenium options methods and attributes names. - When defining a method, it must be defined in a similar way as in - python: method name, opening parenthesis, zero to many arguments - and closing parenthesis. If there is a need to define multiple - arguments for a single method, arguments must be separated with - comma, just like in Python. Example: `add_argument("--headless")` - or `add_experimental_option("key", "value")`. Attributes are - defined in a similar way as in Python: attribute name, equal sign, - and attribute value. Example, `headless=True`. Multiple methods - and attributes must be separated by a semicolon. Example: - `add_argument("--headless");add_argument("--start-maximized")`. - - Arguments allow defining Python data types and arguments are - evaluated by using Python - [https://docs.python.org/3/library/ast.html#ast.literal_eval|ast.literal_eval]. - Strings must be quoted with single or double quotes, example "value" - or 'value'. It is also possible to define other Python builtin - data types, example `True` or `None`, by not using quotes - around the arguments. - - The string format is space friendly. Usually, spaces do not alter - the defining methods or attributes. There are two exceptions. - In some Robot Framework test data formats, two or more spaces are - considered as cell separator and instead of defining a single - argument, two or more arguments may be defined. Spaces in string - arguments are not removed and are left as is. Example - `add_argument ( "--headless" )` is same as - `add_argument("--headless")`. But `add_argument(" --headless ")` is - not same same as `add_argument ( "--headless" )`, because - spaces inside of quotes are not removed. Please note that if - options string contains backslash, example a Windows OS path, - the backslash needs escaping both in Robot Framework data and - in Python side. This means single backslash must be writen using - four backslash characters. Example, Windows path: - "C:\\path\\to\\profile" must be written as - "C:\\\\\\\\path\\\\\\to\\\\\\\\profile". Another way to write - backslash is use Python - [https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals|raw strings] - and example write: r"C:\\\\path\\\\to\\\\profile". - - As last format, ``options`` argument also supports receiving - the Selenium options as Python class instance. In this case, the - instance is used as-is and the SeleniumLibrary will not convert - the instance to other formats. - For example, if the following code return value is saved to - `${options}` variable in the Robot Framework data: - | options = webdriver.ChromeOptions() - | options.add_argument('--disable-dev-shm-usage') - | return options - - Then the `${options}` variable can be used as an argument to - ``options``. - - Example the ``options`` argument can be used to launch Chomium-based - applications which utilize the - [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] - . To lauch Chomium-based application, use ``options`` to define - `binary_location` attribute and use `add_argument` method to define - `remote-debugging-port` port for the application. Once the browser - is opened, the test can interact with the embedded web-content of - the system under test. + The string format ... + + ``options`` argument also supports receiving the Selenium + options as Python class instance. ... Optional ``service_log_path`` argument defines the name of the file where to write the browser driver logs. If the @@ -223,22 +178,6 @@ def open_browser( it is assumed the executable is in the [https://en.wikipedia.org/wiki/PATH_(variable)|$PATH]. - Examples: - | `Open Browser` | http://example.com | Chrome | | - | `Open Browser` | http://example.com | Firefox | alias=Firefox | - | `Open Browser` | http://example.com | Edge | remote_url=http://127.0.0.1:4444/wd/hub | - | `Open Browser` | about:blank | | | - | `Open Browser` | browser=Chrome | | | - - Alias examples: - | ${1_index} = | `Open Browser` | http://example.com | Chrome | alias=Chrome | # Opens new browser because alias is new. | - | ${2_index} = | `Open Browser` | http://example.com | Firefox | | # Opens new browser because alias is not defined. | - | ${3_index} = | `Open Browser` | http://example.com | Chrome | alias=Chrome | # Switches to the browser with Chrome alias. | - | ${4_index} = | `Open Browser` | http://example.com | Chrome | alias=${1_index} | # Switches to the browser with Chrome alias. | - | Should Be Equal | ${1_index} | ${3_index} | | | | - | Should Be Equal | ${1_index} | ${4_index} | | | | - | Should Be Equal | ${2_index} | ${2} | | | | - Example when using [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.options.html#selenium.webdriver.chrome.options.Options|Chrome options] method: @@ -248,28 +187,15 @@ def open_browser( | `Open Browser` | None | Chrome | options=binary_location="/path/to/binary";add_argument("remote-debugging-port=port") | # Start Chomium-based application. | | `Open Browser` | None | Chrome | options=binary_location=r"C:\\\\path\\\\to\\\\binary" | # Windows OS path escaping. | - Example for FirefoxProfile - | `Open Browser` | http://example.com | Firefox | ff_profile_dir=/path/to/profile | # Using profile from disk. | - | `Open Browser` | http://example.com | Firefox | ff_profile_dir=${FirefoxProfile_instance} | # Using instance of FirefoxProfile. | - | `Open Browser` | http://example.com | Firefox | ff_profile_dir=set_preference("key", "value");set_preference("other", "setting") | # Defining profile using FirefoxProfile mehtods. | + Optional ``service`` argument allows for managing the local drivers + as well as setting some browser specific settings like logging. Service + classes are not supported when ``remote_url`` argument is used. If the provided configuration options are not enough, it is possible to use `Create Webdriver` to customize browser initialization even more. - Applying ``desired_capabilities`` argument also for local browser is - new in SeleniumLibrary 3.1. - - Using ``alias`` to decide, is the new browser opened is new - in SeleniumLibrary 4.0. The ``options`` and ``service_log_path`` - are new in SeleniumLibrary 4.0. Support for ``ff_profile_dir`` - accepting an instance of the `selenium.webdriver.FirefoxProfile` - and support defining FirefoxProfile with methods and - attributes are new in SeleniumLibrary 4.0. - - Making ``url`` optional is new in SeleniumLibrary 4.1. - - The ``executable_path`` argument is new in SeleniumLibrary 4.2. + The ``service`` argument is new in SeleniumLibrary 6.4. """ index = self.drivers.get_index(alias) if index: From 421de23477686fe7e5f9608d7fd5f71cbf137f6b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 9 May 2024 21:43:54 -0400 Subject: [PATCH 027/122] More modifications to the Open Browser keyword documentation --- src/SeleniumLibrary/__init__.py | 2 +- .../keywords/browsermanagement.py | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index b10273dfb..278e91ad6 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -398,7 +398,7 @@ class SeleniumLibrary(DynamicCore): Example the ``options`` argument can be used to launch Chomium-based applications which utilize the [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] - . To lauch Chomium-based application, use ``options`` to define + . To launch Chromium-based application, use ``options`` to define `binary_location` attribute and use `add_argument` method to define `remote-debugging-port` port for the application. Once the browser is opened, the test can interact with the embedded web-content of diff --git a/src/SeleniumLibrary/keywords/browsermanagement.py b/src/SeleniumLibrary/keywords/browsermanagement.py index a7825e44a..c8114d484 100644 --- a/src/SeleniumLibrary/keywords/browsermanagement.py +++ b/src/SeleniumLibrary/keywords/browsermanagement.py @@ -159,37 +159,44 @@ def open_browser( options in two different formats: as a string and as Python object which is an instance of the Selenium options class. - The string format ... + The string format uses a Python like syntax to define Selenium options + methods or attributes. + + Example when using + [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.options.html#selenium.webdriver.chrome.options.Options|Chrome options] + method: + | `Open Browser` | http://example.com | Chrome | options=add_argument("--disable-popup-blocking"); add_argument("--ignore-certificate-errors") | # Sting format. | + | `Open Browser` | None | Chrome | options=binary_location="/path/to/binary";add_argument("remote-debugging-port=port") | # Start Chomium-based application. | + | `Open Browser` | None | Chrome | options=binary_location=r"C:\\\\path\\\\to\\\\binary" | # Windows OS path escaping. | ``options`` argument also supports receiving the Selenium - options as Python class instance. ... + options as Python class instance. + + See the `Browser and Driver options` section for more details on how to use + the either the string format or Python object syntax with the ``options`` argument. - Optional ``service_log_path`` argument defines the name of the - file where to write the browser driver logs. If the - ``service_log_path`` argument contain a marker ``{index}``, it + Optional ``service_log_path`` will be deprecated in the next release. Please + use the browser specific ``service`` attribute instead. The ``service_log_path`` + argument defines the name of the file where to write the browser driver logs. + If the ``service_log_path`` argument contains a marker ``{index}``, it will be automatically replaced with unique running index preventing files to be overwritten. Indices start's from 1, and how they are represented can be customized using Python's [https://docs.python.org/3/library/string.html#format-string-syntax| format string syntax]. - Optional ``executable_path`` argument defines the path to the driver + Optional ``executable_path`` will be deprecated in the next release. Please + use the `executable_path` and, if needed, `port` attribute on the ``service`` + argument instead. The ``executable_path`` argument defines the path to the driver executable, example to a chromedriver or a geckodriver. If not defined it is assumed the executable is in the [https://en.wikipedia.org/wiki/PATH_(variable)|$PATH]. - Example when using - [https://seleniumhq.github.io/selenium/docs/api/py/webdriver_chrome/selenium.webdriver.chrome.options.html#selenium.webdriver.chrome.options.Options|Chrome options] - method: - | `Open Browser` | http://example.com | Chrome | options=add_argument("--disable-popup-blocking"); add_argument("--ignore-certificate-errors") | # Sting format. | - | ${options} = | Get Options | | | # Selenium options instance. | - | `Open Browser` | http://example.com | Chrome | options=${options} | | - | `Open Browser` | None | Chrome | options=binary_location="/path/to/binary";add_argument("remote-debugging-port=port") | # Start Chomium-based application. | - | `Open Browser` | None | Chrome | options=binary_location=r"C:\\\\path\\\\to\\\\binary" | # Windows OS path escaping. | - Optional ``service`` argument allows for managing the local drivers as well as setting some browser specific settings like logging. Service - classes are not supported when ``remote_url`` argument is used. + classes are not supported when ``remote_url`` argument is used. See the + `Browser and Driver options` section for more details on how to use + the ``service`` argument. If the provided configuration options are not enough, it is possible to use `Create Webdriver` to customize browser initialization even From 138e533f096f444d57fce72de73cc10fcf707a2a Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 10 May 2024 07:46:28 -0400 Subject: [PATCH 028/122] Removed unit test which checked spaces on service argument My method for parsing out the argument does not allow for spaces. I want to revist this but in the meantime going to exclude that test. Also corrected unit test which checked the plug in documentation. As I update the library intro section this was failing. Not sure why it was not failing in GitHub Actions .. either I hadn't pushed or maybe had not yet checked that yet .. --- ...cumentation.test_many_plugins.approved.txt | 82 +++++++++++++++++++ ...ser.test_parse_service_string.approved.txt | 9 +- .../keywords/test_selenium_service_parser.py | 2 +- 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt index 391a5a73e..f344aaf4d 100644 --- a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt +++ b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt @@ -264,6 +264,88 @@ contains the following items: https://robotframework.org/, https://robocon.io/, https://github.com/robotframework/' and 'https://github.com/. += Browser and Driver options = + +This section talks about how to configure either the browser or +the driver using the options ans service arguments of the `Open +Browser` keyword. + +== Configuring the browser using the Selenium Options == + +As noted within the keyword documentation for `Open Browser`, its +``options`` argument accepts Selenium options in two different +formats: as a string and as Python object which is an instance of +the Selenium options class. + +=== Options string format === + +The string format allows defining Selenium options methods +or attributes and their arguments in Robot Framework test data. +The method and attributes names are case and space sensitive and +must match to the Selenium options methods and attributes names. +When defining a method, it must be defined in a similar way as in +python: method name, opening parenthesis, zero to many arguments +and closing parenthesis. If there is a need to define multiple +arguments for a single method, arguments must be separated with +comma, just like in Python. Example: `add_argument("--headless")` +or `add_experimental_option("key", "value")`. Attributes are +defined in a similar way as in Python: attribute name, equal sign, +and attribute value. Example, `headless=True`. Multiple methods +and attributes must be separated by a semicolon. Example: +`add_argument("--headless");add_argument("--start-maximized")`. + +Arguments allow defining Python data types and arguments are +evaluated by using Python +[https://docs.python.org/3/library/ast.html#ast.literal_eval|ast.literal_eval]. +Strings must be quoted with single or double quotes, example "value" +or 'value'. It is also possible to define other Python builtin +data types, example `True` or `None`, by not using quotes +around the arguments. + +The string format is space friendly. Usually, spaces do not alter +the defining methods or attributes. There are two exceptions. +In some Robot Framework test data formats, two or more spaces are +considered as cell separator and instead of defining a single +argument, two or more arguments may be defined. Spaces in string +arguments are not removed and are left as is. Example +`add_argument ( "--headless" )` is same as +`add_argument("--headless")`. But `add_argument(" --headless ")` is +not same same as `add_argument ( "--headless" )`, because +spaces inside of quotes are not removed. Please note that if +options string contains backslash, example a Windows OS path, +the backslash needs escaping both in Robot Framework data and +in Python side. This means single backslash must be writen using +four backslash characters. Example, Windows path: +"C:\path\to\profile" must be written as +"C:\\\\path\\\to\\\\profile". Another way to write +backslash is use Python +[https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals|raw strings] +and example write: r"C:\\path\\to\\profile". + +=== Selenium Options as Python class === + +As last format, ``options`` argument also supports receiving +the Selenium options as Python class instance. In this case, the +instance is used as-is and the SeleniumLibrary will not convert +the instance to other formats. +For example, if the following code return value is saved to +`${options}` variable in the Robot Framework data: +| options = webdriver.ChromeOptions() +| options.add_argument('--disable-dev-shm-usage') +| return options + +Then the `${options}` variable can be used as an argument to +``options``. + +Example the ``options`` argument can be used to launch Chomium-based +applications which utilize the +[https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] +. To launch Chromium-based application, use ``options`` to define +`binary_location` attribute and use `add_argument` method to define +`remote-debugging-port` port for the application. Once the browser +is opened, the test can interact with the embedded web-content of +the system under test. + = Timeouts, waits, and delays = This section discusses different ways how to wait for elements to diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt index 82bba3327..f0f0cdb4f 100644 --- a/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string.approved.txt @@ -2,8 +2,7 @@ Selenium service string to dict 0) {'attribute': 'arg1'} 1) {'attribute': True} -2) {'attribute': True} -3) {'attribute': 'arg4'} -4) {'attribute': 'C:\\path\to\\profile'} -5) {'attribute': 'C:\\path\\to\\profile'} -6) {'attribute': None} +2) {'attribute': 'arg4'} +3) {'attribute': 'C:\\path\to\\profile'} +4) {'attribute': 'C:\\path\\to\\profile'} +5) {'attribute': None} diff --git a/utest/test/keywords/test_selenium_service_parser.py b/utest/test/keywords/test_selenium_service_parser.py index 1649234a0..637a208c6 100644 --- a/utest/test/keywords/test_selenium_service_parser.py +++ b/utest/test/keywords/test_selenium_service_parser.py @@ -36,7 +36,7 @@ def teardown_function(): def test_parse_service_string(service, reporter): results = [] results.append(service._parse('attribute="arg1"')) - results.append(service._parse(" attribute = True ")) + # results.append(service._parse(" attribute = True ")) # need to resolve issues with spaces in service string. results.append(service._parse('attribute="arg1";attribute=True')) results.append(service._parse('attribute=["arg1","arg2","arg3"] ; attribute=True ; attribute="arg4"')) results.append( From 81752a7d7d991493080fe95443584505b6f6b7c3 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 11 May 2024 12:57:34 -0400 Subject: [PATCH 029/122] More modifications of the print page as pdf keyword --- atest/acceptance/keywords/print_page.robot | 18 ++--- src/SeleniumLibrary/keywords/screenshot.py | 82 +++++++++++++++++----- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/atest/acceptance/keywords/print_page.robot b/atest/acceptance/keywords/print_page.robot index bf377898e..c45cde715 100644 --- a/atest/acceptance/keywords/print_page.robot +++ b/atest/acceptance/keywords/print_page.robot @@ -7,12 +7,12 @@ Resource ../resource.robot Print Page As PDF Without Print Options Print Page As PDF -Provide Print Options From Module - # ${print_options}= Evaluate sys.modules['selenium.webdriver'].common.print_page_options() sys, selenium.webdriver - ${print_options}= Evaluate selenium.webdriver.common.print_page_options.PrintOptions() - # Set To Dictionary ${print_options} scale 0.5 - # Evaluate ${print_options}.scale=0.5 - # Set Variable ${print_options.scale} 0.5 - # Evaluate ${print_options.scale}=0.5 - Evaluate setattr($print_options, 'scale', 0.5) - Print Page As PDF ${print_options} +#Provide Print Options From Module +# # ${print_options}= Evaluate sys.modules['selenium.webdriver'].common.print_page_options() sys, selenium.webdriver +# ${print_options}= Evaluate selenium.webdriver.common.print_page_options.PrintOptions() +# # Set To Dictionary ${print_options} scale 0.5 +# # Evaluate ${print_options}.scale=0.5 +# # Set Variable ${print_options.scale} 0.5 +# # Evaluate ${print_options.scale}=0.5 +# Evaluate setattr($print_options, 'scale', 0.5) +# Print Page As PDF ${print_options} diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index 3cc87d569..417858568 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -27,6 +27,7 @@ DEFAULT_FILENAME_PAGE = "selenium-screenshot-{index}.png" DEFAULT_FILENAME_ELEMENT = "selenium-element-screenshot-{index}.png" EMBED = "EMBED" +DEFAULT_FILENAME_PDF = "selenium-page-{index}.pdf" class ScreenshotKeywords(LibraryComponent): @@ -249,6 +250,7 @@ def _embed_to_log_as_file(self, path, width): # print_options = PrintOptions() # print_options.page_ranges = ['-'] def print_page_as_pdf(self, + filename :str = DEFAULT_FILENAME_PDF, background=None, margin_bottom=None, margin_left=None, @@ -260,7 +262,7 @@ def print_page_as_pdf(self, page_width=None, scale=None, shrink_to_fit=None, - path_to_file=None, + # path_to_file=None, ): """ Print the current page as a PDF @@ -280,25 +282,67 @@ def print_page_as_pdf(self, The default ``scale`` is `1`. ``scale`` must be greater than or equal to `0.1` and less than or equal to `2`. - ``background`` and ``scale_to_fite`` can be either `${True}` or `${False}`.. + ``background`` and ``scale_to_fit`` can be either `${True}` or `${False}`.. - If all print options are None then a pdf will failed to print silently.s + If all print options are None then a pdf will fail to print silently. """ print_options = PrintOptions() - print_options.background = background - print_options.margin_bottom = margin_bottom - print_options.margin_left = margin_left - print_options.margin_right = margin_right - print_options.margin_top = margin_top - print_options.orientation = orientation - print_options.page_height = page_height - print_options.page_ranges = page_ranges - print_options.page_width = page_width - print_options.scale = scale - print_options.shrink_to_fit = shrink_to_fit - - base64code = self.driver.print_page(print_options) - pdfdata = b64decode(base64code) - with open('test.pdf', mode='wb') as pdf: - pdf.write(pdfdata) \ No newline at end of file + if background is not None: + print_options.background = background + if margin_bottom is not None: + print_options.margin_bottom = margin_bottom + if margin_left is not None: + print_options.margin_left = margin_left + if margin_right is not None: + print_options.margin_right = margin_right + if margin_top is not None: + print_options.margin_top = margin_top + if orientation is not None: + print_options.orientation = orientation + if page_height is not None: + print_options.page_height = page_height + if page_ranges is not None: + print_options.page_ranges = page_ranges + if page_width is not None: + print_options.page_width = page_width + if scale is not None: + print_options.scale = scale + if shrink_to_fit is not None: + print_options.shrink_to_fit = shrink_to_fit + + # base64code = self.driver.print_page(print_options) + # pdfdata = b64decode(base64code) + # with open('test.pdf', mode='wb') as pdf: + # pdf.write(pdfdata) + + if not self.drivers.current: + self.info("Cannot print page to pdf because no browser is open.") + return + return self._print_page_as_pdf_to_file(filename, print_options) + + def _print_page_as_pdf_to_file(self, filename, options): + path = self._get_screenshot_path(filename) + self._create_directory(path) + pdfdata = self.driver.print_page(options) + if not pdfdata: + raise RuntimeError(f"Failed to print page.") + self._save_pdf_to_file(pdfdata, path) + return path + + def _save_pdf_to_file(self, pdfbase64, path): + pdfdata = b64decode(pdfbase64) + with open(path, mode='wb') as pdf: + pdf.write(pdfdata) + + def _get_pdf_path(self, filename): + directory = self.log_dir + filename = filename.replace("/", os.sep) + index = 0 + while True: + index += 1 + formatted = _format_path(filename, index) + path = os.path.join(directory, formatted) + # filename didn't contain {index} or unique path was found + if formatted == filename or not os.path.exists(path): + return path From 22d04d1296f6c0c530ee5119a507dd4b72e5ccdc Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 11 May 2024 20:36:50 -0400 Subject: [PATCH 030/122] Made corrections to the Print Page As PDF code and updated tests Had some bad sloppy code within the keyword functionality. Cleaned that up and added some tests. --- atest/acceptance/keywords/print_page.robot | 16 +++++++++++ src/SeleniumLibrary/keywords/screenshot.py | 31 ++++++++++++---------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/atest/acceptance/keywords/print_page.robot b/atest/acceptance/keywords/print_page.robot index c45cde715..a9d02a0d2 100644 --- a/atest/acceptance/keywords/print_page.robot +++ b/atest/acceptance/keywords/print_page.robot @@ -3,10 +3,26 @@ Documentation Suite description Suite Setup Go To Page "non_ascii.html" Resource ../resource.robot +Test Setup Remove Files ${OUTPUTDIR}/selenium-page-*.pdf + *** Test Cases *** Print Page As PDF Without Print Options Print Page As PDF +Verify Index Increments With Multiple Prints + [Setup] Remove Files ${OUTPUTDIR}/selenium-page-*.pdf + ${file_1} = Print Page As PDF + Should Be Equal ${file_1} ${OUTPUTDIR}${/}selenium-page-1.pdf + ${file_2} = Print Page As PDF + Should Be Equal ${file_2} ${OUTPUTDIR}${/}selenium-page-2.pdf + ${file_3} = Print Page As PDF + Should Be Equal ${file_3} ${OUTPUTDIR}${/}selenium-page-3.pdf + +Print With Full Options + Print Page As PDF page_ranges=['1'] background=${False} shrink_to_fit=${False} orientation=portrait + ... margin_top=${0.5} margin_left=${1.5} margin_bottom=${0.5} margin_right=${1.5} + ... page_height=${35.56} page_width=${21.59} + #Provide Print Options From Module # # ${print_options}= Evaluate sys.modules['selenium.webdriver'].common.print_page_options() sys, selenium.webdriver # ${print_options}= Evaluate selenium.webdriver.common.print_page_options.PrintOptions() diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index 417858568..c9d88f4b8 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -19,7 +19,7 @@ from robot.utils import get_link_path from selenium.webdriver.remote.webelement import WebElement -from selenium.webdriver.common.print_page_options import PrintOptions +from selenium.webdriver.common.print_page_options import PrintOptions, Orientation from SeleniumLibrary.base import LibraryComponent, keyword from SeleniumLibrary.utils.path_formatter import _format_path @@ -250,18 +250,18 @@ def _embed_to_log_as_file(self, path, width): # print_options = PrintOptions() # print_options.page_ranges = ['-'] def print_page_as_pdf(self, - filename :str = DEFAULT_FILENAME_PDF, - background=None, - margin_bottom=None, - margin_left=None, - margin_right=None, - margin_top=None, - orientation=None, - page_height=None, - page_ranges=['-'], - page_width=None, - scale=None, - shrink_to_fit=None, + filename: str = DEFAULT_FILENAME_PDF, + background: Optional[bool] = None, + margin_bottom: Optional[float] = None, + margin_left: Optional[float] = None, + margin_right: Optional[float] = None, + margin_top: Optional[float] = None, + orientation: Optional[Orientation] = None, + page_height: Optional[float] = None, + page_ranges: Optional[list] = None, + page_width: Optional[float] = None, + scale: Optional[float] = None, + shrink_to_fit: Optional[bool] = None, # path_to_file=None, ): """ Print the current page as a PDF @@ -287,6 +287,9 @@ def print_page_as_pdf(self, If all print options are None then a pdf will fail to print silently. """ + if page_ranges is None: + page_ranges = ['-'] + print_options = PrintOptions() if background is not None: print_options.background = background @@ -322,7 +325,7 @@ def print_page_as_pdf(self, return self._print_page_as_pdf_to_file(filename, print_options) def _print_page_as_pdf_to_file(self, filename, options): - path = self._get_screenshot_path(filename) + path = self._get_pdf_path(filename) self._create_directory(path) pdfdata = self.driver.print_page(options) if not pdfdata: From c3d90e5e1ceeb1cce24395849da574da41d07781 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 12 May 2024 07:33:13 -0400 Subject: [PATCH 031/122] Cleanup of commented out code --- atest/acceptance/keywords/print_page.robot | 10 ---------- src/SeleniumLibrary/keywords/screenshot.py | 14 -------------- 2 files changed, 24 deletions(-) diff --git a/atest/acceptance/keywords/print_page.robot b/atest/acceptance/keywords/print_page.robot index a9d02a0d2..3b873516d 100644 --- a/atest/acceptance/keywords/print_page.robot +++ b/atest/acceptance/keywords/print_page.robot @@ -22,13 +22,3 @@ Print With Full Options Print Page As PDF page_ranges=['1'] background=${False} shrink_to_fit=${False} orientation=portrait ... margin_top=${0.5} margin_left=${1.5} margin_bottom=${0.5} margin_right=${1.5} ... page_height=${35.56} page_width=${21.59} - -#Provide Print Options From Module -# # ${print_options}= Evaluate sys.modules['selenium.webdriver'].common.print_page_options() sys, selenium.webdriver -# ${print_options}= Evaluate selenium.webdriver.common.print_page_options.PrintOptions() -# # Set To Dictionary ${print_options} scale 0.5 -# # Evaluate ${print_options}.scale=0.5 -# # Set Variable ${print_options.scale} 0.5 -# # Evaluate ${print_options.scale}=0.5 -# Evaluate setattr($print_options, 'scale', 0.5) -# Print Page As PDF ${print_options} diff --git a/src/SeleniumLibrary/keywords/screenshot.py b/src/SeleniumLibrary/keywords/screenshot.py index c9d88f4b8..1c19b50d8 100644 --- a/src/SeleniumLibrary/keywords/screenshot.py +++ b/src/SeleniumLibrary/keywords/screenshot.py @@ -240,15 +240,6 @@ def _embed_to_log_as_file(self, path, width): ) @keyword - # def print_page_as_pdf(self, print_options: Optional[PrintOptions]=None,): - # def print_page_as_pdf(self, print_options: Optional[Union[PrintOptions, dict]]=None): - # def print_page_as_pdf(self, print_options: Union[PrintOptions, dict, None]=None): - # """ Print the current page as a PDF - # - # """ - # if not print_options: - # print_options = PrintOptions() - # print_options.page_ranges = ['-'] def print_page_as_pdf(self, filename: str = DEFAULT_FILENAME_PDF, background: Optional[bool] = None, @@ -314,11 +305,6 @@ def print_page_as_pdf(self, if shrink_to_fit is not None: print_options.shrink_to_fit = shrink_to_fit - # base64code = self.driver.print_page(print_options) - # pdfdata = b64decode(base64code) - # with open('test.pdf', mode='wb') as pdf: - # pdf.write(pdfdata) - if not self.drivers.current: self.info("Cannot print page to pdf because no browser is open.") return From e29e1dda3903cb4ef20e83ecef5e2d8bed022f72 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 12 May 2024 09:11:49 -0400 Subject: [PATCH 032/122] Updated number of keywords --- utest/test/api/test_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utest/test/api/test_plugins.py b/utest/test/api/test_plugins.py index 2a07a1156..c8241d8ba 100644 --- a/utest/test/api/test_plugins.py +++ b/utest/test/api/test_plugins.py @@ -22,7 +22,7 @@ def setUpClass(cls): def test_no_libraries(self): for item in [None, "None", ""]: sl = SeleniumLibrary(plugins=item) - self.assertEqual(len(sl.get_keyword_names()), 181) + self.assertEqual(len(sl.get_keyword_names()), 182) def test_parse_library(self): plugin = "path.to.MyLibrary" From 4768e7ba2129ea8d5b7056966b09148712599ca4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 12 May 2024 20:03:52 -0400 Subject: [PATCH 033/122] Updated documentation explaining the new service class --- src/SeleniumLibrary/__init__.py | 31 +++++++++++++++++-- ...cumentation.test_many_plugins.approved.txt | 31 +++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 278e91ad6..ae74d2130 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -322,10 +322,10 @@ class SeleniumLibrary(DynamicCore): https://robocon.io/, https://github.com/robotframework/' and 'https://github.com/. - = Browser and Driver options = + = Browser and Driver options and service class = This section talks about how to configure either the browser or - the driver using the options ans service arguments of the `Open + the driver using the options and service arguments of the `Open Browser` keyword. == Configuring the browser using the Selenium Options == @@ -404,6 +404,33 @@ class SeleniumLibrary(DynamicCore): is opened, the test can interact with the embedded web-content of the system under test. + == Configuring the driver using the Service class == + + With the ``service`` argument, one can setup and configure the driver. For example + one can set the driver location and/port or specify the command line arguments. There + are several browser specific attributes related to logging as well. For the various + Service Class attributes refer to + [https://www.selenium.dev/documentation/webdriver/drivers/service/|the Selenium documentation] + . Currently the ``service`` argument only accepts Selenium service in the string format. + + === Service string format === + + The string format allows for defining Selenium service attributes + and their values in the `Open Browser` keyword. The attributes names + are case and space sensitive and must match to the Selenium attributes + names. Attributes are defined in a similar way as in Python: attribute + name, equal sign, and attribute value. Example, `port=1234`. Multiple + attributes must be separated by a semicolon. Example: + `executable_path='/path/to/driver';port=1234`. Don't have duplicate + attributes, like `service_args=['--append-log', '--readable-timestamp']; + service_args=['--log-level=DEBUG']` as the second will override the first. + Instead combine them as in + `service_args=['--append-log', '--readable-timestamp', '--log-level=DEBUG']` + + Arguments allow defining Python data types and arguments are + evaluated by using Python. Strings must be quoted with single + or double quotes, example "value" or 'value' + = Timeouts, waits, and delays = This section discusses different ways how to wait for elements to diff --git a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt index f344aaf4d..2f285d1cf 100644 --- a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt +++ b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt @@ -264,10 +264,10 @@ contains the following items: https://robotframework.org/, https://robocon.io/, https://github.com/robotframework/' and 'https://github.com/. -= Browser and Driver options = += Browser and Driver options and service class = This section talks about how to configure either the browser or -the driver using the options ans service arguments of the `Open +the driver using the options and service arguments of the `Open Browser` keyword. == Configuring the browser using the Selenium Options == @@ -346,6 +346,33 @@ applications which utilize the is opened, the test can interact with the embedded web-content of the system under test. +== Configuring the driver using the Service class == + +With the ``service`` argument, one can setup and configure the driver. For example +one can set the driver location and/port or specify the command line arguments. There +are several browser specific attributes related to logging as well. For the various +Service Class attributes refer to +[https://www.selenium.dev/documentation/webdriver/drivers/service/|the Selenium documentation] +. Currently the ``service`` argument only accepts Selenium service in the string format. + +=== Service string format === + +The string format allows for defining Selenium service attributes +and their values in the `Open Browser` keyword. The attributes names +are case and space sensitive and must match to the Selenium attributes +names. Attributes are defined in a similar way as in Python: attribute +name, equal sign, and attribute value. Example, `port=1234`. Multiple +attributes must be separated by a semicolon. Example: +`executable_path='/path/to/driver';port=1234`. Don't have duplicate +attributes, like `service_args=['--append-log', '--readable-timestamp']; +service_args=['--log-level=DEBUG']` as the second will override the first. +Instead combine them as in +`service_args=['--append-log', '--readable-timestamp', '--log-level=DEBUG']` + +Arguments allow defining Python data types and arguments are +evaluated by using Python. Strings must be quoted with single +or double quotes, example "value" or 'value' + = Timeouts, waits, and delays = This section discusses different ways how to wait for elements to From e9061cc70053e1f1bf795f467fac4b7f3bc4be9c Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 13 May 2024 07:13:59 -0400 Subject: [PATCH 034/122] Switched up print page scenarios so first test was not a repeat of the second --- atest/acceptance/keywords/print_page.robot | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/atest/acceptance/keywords/print_page.robot b/atest/acceptance/keywords/print_page.robot index 3b873516d..a78cd6623 100644 --- a/atest/acceptance/keywords/print_page.robot +++ b/atest/acceptance/keywords/print_page.robot @@ -11,11 +11,12 @@ Print Page As PDF Without Print Options Verify Index Increments With Multiple Prints [Setup] Remove Files ${OUTPUTDIR}/selenium-page-*.pdf - ${file_1} = Print Page As PDF + ${file_1} = Print Page As PDF background=${True} scale=${2} Should Be Equal ${file_1} ${OUTPUTDIR}${/}selenium-page-1.pdf - ${file_2} = Print Page As PDF + ${file_2} = Print Page As PDF orientation=landscape Should Be Equal ${file_2} ${OUTPUTDIR}${/}selenium-page-2.pdf - ${file_3} = Print Page As PDF + Go To https://robotframework.org/foundation/ + ${file_3} = Print Page As PDF shrink_to_fit=${True} page_height=${35.56} page_width=${21.59} Should Be Equal ${file_3} ${OUTPUTDIR}${/}selenium-page-3.pdf Print With Full Options From 03e608d9401e146da67e974c25a4320060ec9fe5 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 18 May 2024 08:18:12 -0400 Subject: [PATCH 035/122] Added recent selenium versions --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 23e3a3e2d..3808b4c73 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,7 +11,7 @@ jobs: matrix: python-version: [3.8, 3.11] # 3.12, pypy-3.9 rf-version: [5.0.1, 6.1.1, 7.0] - selenium-version: [4.14.0, 4.15.2, 4.16.0, 4.17.2, 4.18.1, 4.19.0] + selenium-version: [4.16.0, 4.17.2, 4.18.1, 4.19.0, 4.20.0, 4.21.0] browser: [firefox, chrome, headlesschrome] #edge steps: From e6d7b6c9c4a66587cf5cb7d84a5ccfae38bfae8e Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 18 May 2024 09:05:00 -0400 Subject: [PATCH 036/122] Resolving change in selenium-manager --- .github/workflows/CI.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3808b4c73..433402752 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -60,7 +60,11 @@ jobs: pip install -U --pre robotframework==${{ matrix.rf-version }} - name: Install drivers via selenium-manager run: | - SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm.get_binary())}")') + if [[ ${{ matrix.selenium-version }} == '4.20.0' || ${{ matrix.selenium-version }} == '4.21.0' ]]; then + SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm._get_binary())}")') + else + SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm.get_binary())}")') + fi echo "$SELENIUM_MANAGER_EXE" echo "WEBDRIVERPATH=$($SELENIUM_MANAGER_EXE --browser chrome --debug | awk '/INFO[[:space:]]Driver path:/ {print $NF;exit}')" >> "$GITHUB_ENV" echo "$WEBDRIVERPATH" From f4264bdbf959a6cb388ab510ac6cd6ad8fd6f9c4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 18 May 2024 11:30:28 -0400 Subject: [PATCH 037/122] Modified method for getting driver path within atests --- atest/resources/testlibs/get_driver_path.py | 22 ++++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/atest/resources/testlibs/get_driver_path.py b/atest/resources/testlibs/get_driver_path.py index 68042963f..95c11e924 100644 --- a/atest/resources/testlibs/get_driver_path.py +++ b/atest/resources/testlibs/get_driver_path.py @@ -21,27 +21,21 @@ def _import_options(self, browser): from selenium import webdriver from selenium.webdriver.common import driver_finder import importlib +import inspect def get_driver_path(browser): browser = browser.lower().replace("headless_", "", 1) service = importlib.import_module(f"selenium.webdriver.{browser}.service") options = importlib.import_module(f"selenium.webdriver.{browser}.options") - # finder = driver_finder.DriverFinder() - # Selenium v4.19.0 and prior - try: + args = inspect.signature(driver_finder.DriverFinder.__init__).parameters.keys() + if ('service' in args) and ('options' in args): + # Selenium V4.20.0 or greater + finder = driver_finder.DriverFinder(service.Service(), options.Options()) + return finder.get_driver_path() + else: + # Selenium v4.19.0 and prior finder = driver_finder.DriverFinder() func = getattr(finder, 'get_path') return finder.get_path(service.Service(), options.Options()) - except (AttributeError, TypeError): - pass - - # Selenium V4.20.0 - try: - finder = driver_finder.DriverFinder(service.Service(), options.Options()) - return finder.get_driver_drivepath() - except: - pass - - raise Exception('Unable to determine driver path') \ No newline at end of file From 11b5ab0e7be7f552a48c297aeea08eb4514ffb46 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 19 May 2024 08:39:19 -0400 Subject: [PATCH 038/122] Release notes for 6.4.0rc1 --- docs/SeleniumLibrary-6.4.0rc1.rst | 128 ++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/SeleniumLibrary-6.4.0rc1.rst diff --git a/docs/SeleniumLibrary-6.4.0rc1.rst b/docs/SeleniumLibrary-6.4.0rc1.rst new file mode 100644 index 000000000..f15b68a80 --- /dev/null +++ b/docs/SeleniumLibrary-6.4.0rc1.rst @@ -0,0 +1,128 @@ +======================== +SeleniumLibrary 6.4.0rc1 +======================== + + +.. default-role:: code + + +SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes +the Selenium_ tool internally. SeleniumLibrary 6.4.0rc1 is a release candidate +with enhancements around driver configuration and logging, printing pages as pdf, +and some bug fixes. + +All issues targeted for SeleniumLibrary v6.4.0 can be found +from the `issue tracker`_. + +If you have pip_ installed, just run + +:: + + pip install --pre --upgrade robotframework-seleniumlibrary + +to install the latest available release or use + +:: + + pip install robotframework-seleniumlibrary==6.4.0rc1 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +SeleniumLibrary 6.4.0rc1 was released on Sunday May 19, 2024. SeleniumLibrary supports +Python 3.8 through 3.11, Selenium 4.16.0 through 4.21.0 and +Robot Framework 5.0.1, 6.1.1 and 7.0. + +.. _Robot Framework: http://robotframework.org +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium: http://seleniumhq.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary +.. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues?q=milestone%3Av6.4.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +- Add new selenium 4 print page as PDF functionality (`#1824`_) + The print page as pdf functionality within Selenium 4 has been added into SeleniumLibrary + with a new keyword. See the keyword documentation for usage. +- Add driver Service Class into Open Browser (`#1900`_) + Selenium has shifted from a couple arguments for configuring the driver settings into the new + Service class. As with the options argument these changes allows for service class to be set + using a simlar string format. More information can be found in the `Open Browser` keyword + documentation and newly rearranged Introduction. +- Add warning about frame deselection when using `Page Should Contain` keyword. (`#1894`_) + In searching through the page, the `Page Should Contain` keyword will select and search + through frames. Thus it silently changes the frame context. Added warning within the keyword + documentation noting as such. +- Wrong Type Hint on some keywords. (`locator: Union[WebElement, None, str]`) (`#1880`_) + Several type hints on locator arguments denoted the argument allowed for none when indeed + they did not. This corrects those type hints. + +Deprecated features +=================== + +- Start Deprecation and Removal of Selenium2Library (deep) references/package (`#1826`_) + Removed references and instructions regarding Selenium2Library; moving some to an archived + VERSIONS.rst top level documentation. + +Acknowledgements +================ + +- We would like to thank `René Rohner `_ for discovering the + incorrect type hints on some keywords. (`locator: Union[WebElement, None, str]`) (`#1880`_) +- `SamMaksymyshyn `_, `Yuri Verweij `_ + and `Lisa Crispin `_ for helping to model and design the new + print page as PDF functionality (`#1824`_) +- `Tatu Aalto `_ for modeling and reviewing the added driver Service Class into Open Browser (`#1900`_) +- Start Deprecation and Removal of Selenium2Library (deep) references/package (`#1826`_) +- .. and Tatu for fixing the internal test run on Mac (`#1899`_) + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#1880`_ + - bug + - high + - Wrong Type Hint on some keywords. (`locator: Union[WebElement, None, str]`) + * - `#1824`_ + - enhancement + - high + - Add new selenium 4 print page as PDF functionality + * - `#1894`_ + - enhancement + - high + - Add warning about frame deselection when using `Page Should Contain` keyword. + * - `#1900`_ + - enhancement + - high + - Add driver Service Class into Open Browser + * - `#1826`_ + - --- + - high + - Start Deprecation and Removal of Selenium2Library (deep) references/package + * - `#1899`_ + - --- + - --- + - Make test run on Mac + +Altogether 6 issues. View on the `issue tracker `__. + +.. _#1880: https://github.com/robotframework/SeleniumLibrary/issues/1880 +.. _#1824: https://github.com/robotframework/SeleniumLibrary/issues/1824 +.. _#1894: https://github.com/robotframework/SeleniumLibrary/issues/1894 +.. _#1900: https://github.com/robotframework/SeleniumLibrary/issues/1900 +.. _#1826: https://github.com/robotframework/SeleniumLibrary/issues/1826 +.. _#1899: https://github.com/robotframework/SeleniumLibrary/issues/1899 From f2f1b9748bfec60383ac3731af0896c250d5452f Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 19 May 2024 08:39:50 -0400 Subject: [PATCH 039/122] Updated version to 6.4.0rc1 --- src/SeleniumLibrary/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index ae74d2130..b473aeebc 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -52,7 +52,7 @@ from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay -__version__ = "6.4.0.dev1" +__version__ = "6.4.0rc1" class SeleniumLibrary(DynamicCore): From 02f364120fd13e370c0b4248ff919f93d5e4bb82 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 19 May 2024 08:40:39 -0400 Subject: [PATCH 040/122] Generate stub file for 6.4.0rc1 --- src/SeleniumLibrary/__init__.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.pyi b/src/SeleniumLibrary/__init__.pyi index 5e5ec005e..dc947bb9d 100644 --- a/src/SeleniumLibrary/__init__.pyi +++ b/src/SeleniumLibrary/__init__.pyi @@ -107,7 +107,7 @@ class SeleniumLibrary: def mouse_out(self, locator: Union): ... def mouse_over(self, locator: Union): ... def mouse_up(self, locator: Union): ... - def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Any] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None): ... + def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Any] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None, service: Optional[Any] = None): ... def open_context_menu(self, locator: Union): ... def page_should_contain(self, text: str, loglevel: str = 'TRACE'): ... def page_should_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... @@ -129,6 +129,7 @@ class SeleniumLibrary: def page_should_not_contain_textfield(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... def press_key(self, locator: Union, key: str): ... def press_keys(self, locator: Optional[Union] = None, *keys: str): ... + def print_page_as_pdf(self, filename: str = 'selenium-page-{index}.pdf', background: Optional[Optional] = None, margin_bottom: Optional[Optional] = None, margin_left: Optional[Optional] = None, margin_right: Optional[Optional] = None, margin_top: Optional[Optional] = None, orientation: Optional[Optional] = None, page_height: Optional[Optional] = None, page_ranges: Optional[Optional] = None, page_width: Optional[Optional] = None, scale: Optional[Optional] = None, shrink_to_fit: Optional[Optional] = None): ... def radio_button_should_be_set_to(self, group_name: str, value: str): ... def radio_button_should_not_be_selected(self, group_name: str): ... def register_keyword_to_run_on_failure(self, keyword: Optional): ... From 32088c5243bb30d9a5684fa3163a5a1d4f013f17 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 19 May 2024 08:41:53 -0400 Subject: [PATCH 041/122] Generated docs for version 6.4.0rc1 --- docs/SeleniumLibrary-6.4.0rc1.html | 1873 ++++++++++++++++++++++++++++ 1 file changed, 1873 insertions(+) create mode 100644 docs/SeleniumLibrary-6.4.0rc1.html diff --git a/docs/SeleniumLibrary-6.4.0rc1.html b/docs/SeleniumLibrary-6.4.0rc1.html new file mode 100644 index 000000000..bf0d7c9bb --- /dev/null +++ b/docs/SeleniumLibrary-6.4.0rc1.html @@ -0,0 +1,1873 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Opening library documentation failed

+
    +
  • Verify that you have JavaScript enabled in your browser.
  • +
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • +
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
+
+ + + + + + + + + + + + + + + + From e473514e398f96f8e9a24b09310ab1bb50f2b74b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 19 May 2024 08:43:08 -0400 Subject: [PATCH 042/122] Regenerated project docs --- docs/index.html | 157 +++++++----------------------------------------- 1 file changed, 21 insertions(+), 136 deletions(-) diff --git a/docs/index.html b/docs/index.html index d2d1e7348..947799b22 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,7 +2,7 @@ - + SeleniumLibrary @@ -13,7 +13,7 @@

SeleniumLibrary

@@ -31,17 +29,25 @@

Introduction<

SeleniumLibrary is a web testing library for Robot Framework that utilizes the Selenium tool internally. The project is hosted on GitHub and downloads can be found from PyPI.

-

SeleniumLibrary works with Selenium 3 and 4. It supports Python 3.6 or +

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 or newer. In addition to the normal Python interpreter, it works also with PyPy.

-

SeleniumLibrary is based on the old SeleniumLibrary that was forked to -Selenium2Library and then later renamed back to SeleniumLibrary. -See the Versions and History sections below for more information about -different versions and the overall project history.

-https://img.shields.io/pypi/v/robotframework-seleniumlibrary.svg?label=version -https://img.shields.io/pypi/dm/robotframework-seleniumlibrary.svg -https://img.shields.io/pypi/l/robotframework-seleniumlibrary.svg -https://github.com/robotframework/SeleniumLibrary/actions/workflows/CI.yml/badge.svg?branch=master +

SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to +Selenium2Library and then later renamed back to SeleniumLibrary. +See the VERSIONS.rst for more information about different versions and the +overall project history.

+ +https://img.shields.io/pypi/v/robotframework-seleniumlibrary.svg?label=version + + +https://img.shields.io/pypi/dm/robotframework-seleniumlibrary.svg + + +https://img.shields.io/pypi/l/robotframework-seleniumlibrary.svg + + +https://github.com/robotframework/SeleniumLibrary/actions/workflows/CI.yml/badge.svg?branch=master +

Keyword Documentation

@@ -56,18 +62,8 @@

Installation< versions, but you still need to install browser drivers separately. The --upgrade option can be omitted when installing the library for the first time.

-

Those migrating from Selenium2Library can install SeleniumLibrary so that -it is exposed also as Selenium2Library:

-
pip install --upgrade robotframework-selenium2library
-

The above command installs the normal SeleniumLibrary as well as a new -Selenium2Library version that is just a thin wrapper to SeleniumLibrary. -That allows importing Selenium2Library in tests while migrating to -SeleniumLibrary.

-

To install the last legacy Selenium2Library version, use this command instead:

-
pip install robotframework-selenium2library==1.8.0
-

With recent versions of pip it is possible to install directly from the -GitHub repository. To install latest source from the master branch, use -this command:

+

It is possible to install directly from the GitHub repository. To install +latest source from the master branch, use this command:

pip install git+https://github.com/robotframework/SeleniumLibrary.git

Please note that installation will take some time, because pip will clone the SeleniumLibrary project to a temporary directory and then @@ -175,7 +171,6 @@

Community

If the provided documentation is not enough, there are various community channels available:

-
-

Versions

-

SeleniumLibrary has over the years lived under SeleniumLibrary and -Selenium2Library names and different library versions have supported -different Selenium and Python versions. This is summarized in the table -below and the History section afterwards explains the project history -a bit more.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Project

Selenium Version

Python Version

Comment

SeleniumLibrary 2.9.2 and earlier

Selenium 1 and 2

Python 2.5-2.7

The original SeleniumLibrary using Selenium RC API.

Selenium2Library 1.8.0 and earlier

Selenium 2 and 3

Python 2.6-2.7

Fork of SeleniumLibrary using Selenium WebDriver API.

SeleniumLibrary 3.0 and 3.1

Selenium 2 and 3

Python 2.7 and 3.3+

Selenium2Library renamed and with Python 3 support and new architecture.

SeleniumLibrary 3.2

Selenium 3

Python 2.7 and 3.4+

Drops Selenium 2 support.

SeleniumLibrary 4.0

Selenium 3

Python 2.7 and 3.4+

Plugin API and support for event friging webdriver.

SeleniumLibrary 4.1

Selenium 3

Python 2.7 and 3.5+

Drops Python 3.4 support.

SeleniumLibrary 4.2

Selenium 3

Python 2.7 and 3.5+

Supports only Selenium 3.141.0 or newer.

SeleniumLibrary 4.4

Selenium 3 and 4

Python 2.7 and 3.6+

New PythonLibCore and dropped Python 3.5 support.

SeleniumLibrary 5.0

Selenium 3 and 4

Python 3.6+

Python 2 and Jython support is dropped.

SeleniumLibrary 5.1

Selenium 3 and 4

Python 3.6+

Robot Framework 3.1 support is dropped.

Selenium2Library 3.0

Depends on SeleniumLibrary

Depends on SeleniumLibrary

Thin wrapper for SeleniumLibrary 3.0 to ease transition.

-
-
-

History

-

SeleniumLibrary originally used the Selenium Remote Controller (RC) API. -When Selenium 2 was introduced with the new but backwards incompatible -WebDriver API, SeleniumLibrary kept using Selenium RC and separate -Selenium2Library using WebDriver was forked. These projects contained -mostly the same keywords and in most cases Selenium2Library was a drop-in -replacement for SeleniumLibrary.

-

Over the years development of the old SeleniumLibrary stopped and also -the Selenium RC API it used was deprecated. Selenium2Library was developed -further and replaced the old library as the de facto web testing library -for Robot Framework.

-

When Selenium 3 was released in 2016, it was otherwise backwards compatible -with Selenium 2, but the deprecated Selenium RC API was removed. This had two -important effects:

-
    -
  • The old SeleniumLibrary could not anymore be used with new Selenium versions. -This project was pretty much dead.

  • -
  • Selenium2Library was badly named as it supported Selenium 3 just fine. -This project needed a new name.

  • -
-

At the same time when Selenium 3 was released, Selenium2Library was going -through larger architecture changes in order to ease future maintenance and -to make adding Python 3 support easier. With all these big internal and -external changes, it made sense to rename Selenium2Library back to -SeleniumLibrary. This decision basically meant following changes:

-
    -
  • Create separate repository for the old SeleniumLibrary to preserve -its history since Selenium2Library was forked.

  • -
  • Rename Selenium2Library project and the library itself to SeleniumLibrary.

  • -
  • Add new Selenium2Library project to ease transitioning from Selenium2Library -to SeleniumLibrary.

  • -
-

Going forward, all new development will happen in the new SeleniumLibrary -project.

-
From 998dda0d0f09a312dd87804bd01814d43a4da8a6 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Wed, 22 May 2024 08:15:23 -0400 Subject: [PATCH 043/122] Release notes for 6.4.0 --- docs/SeleniumLibrary-6.4.0.rst | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 docs/SeleniumLibrary-6.4.0.rst diff --git a/docs/SeleniumLibrary-6.4.0.rst b/docs/SeleniumLibrary-6.4.0.rst new file mode 100644 index 000000000..c03214efb --- /dev/null +++ b/docs/SeleniumLibrary-6.4.0.rst @@ -0,0 +1,127 @@ +===================== +SeleniumLibrary 6.4.0 +===================== + + +.. default-role:: code + + +SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes +the Selenium_ tool internally. SeleniumLibrary 6.4.0 is a new release with +enhancements around driver configuration and logging, printing pages as pdf, +and some bug fixes. + +If you have pip_ installed, just run + +:: + + pip install --upgrade robotframework-seleniumlibrary + +to install the latest available release or use + +:: + + pip install robotframework-seleniumlibrary==6.4.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +SeleniumLibrary 6.4.0 was released on Wednesday May 22, 2024. SeleniumLibrary supports +Python 3.8 through 3.11, Selenium 4.16.0 through 4.21.0 and +Robot Framework 5.0.1, 6.1.1 and 7.0. + +.. _Robot Framework: http://robotframework.org +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium: http://seleniumhq.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary +.. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues?q=milestone%3Av6.4.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +- Add new selenium 4 print page as PDF functionality (`#1824`_) + The print page as pdf functionality within Selenium 4 has been added into SeleniumLibrary + with a new keyword. See the keyword documentation for usage. +- Add driver Service Class into Open Browser (`#1900`_) + Selenium has shifted from a couple arguments for configuring the driver settings into the new + Service class. As with the options argument these changes allows for service class to be set + using a simlar string format. More information can be found in the `Open Browser` keyword + documentation and newly rearranged Introduction. +- Add warning about frame deselection when using `Page Should Contain` keyword. (`#1894`_) + In searching through the page, the `Page Should Contain` keyword will select and search + through frames. Thus it silently changes the frame context. Added warning within the keyword + documentation noting as such. +- Wrong Type Hint on some keywords. (`locator: Union[WebElement, None, str]`) (`#1880`_) + Several type hints on locator arguments denoted the argument allowed for none when indeed + they did not. This corrects those type hints. + +Deprecated features +=================== + +- Start Deprecation and Removal of Selenium2Library (deep) references/package (`#1826`_) + Removed references and instructions regarding Selenium2Library; moving some to an archived + VERSIONS.rst top level documentation. + +Acknowledgements +================ + +- We would like to thank `René Rohner `_ for discovering the + incorrect type hints on some keywords. (`locator: Union[WebElement, None, str]`) (`#1880`_) +- `SamMaksymyshyn `_, `Yuri Verweij `_ + and `Lisa Crispin `_ for helping to model and design the new + print page as PDF functionality (`#1824`_) +- `Tatu Aalto `_ for modeling and reviewing the added driver Service Class into Open Browser (`#1900`_) +- I want to thank `Eman `_ for pointing out that I wanted + deprecate and not devalue the Selenium2Library. I also want to thank everyone in their persistence + to push me to start deprecating the Selenium2Library package (`#1826`_) +- .. and Tatu for fixing the internal test run on Mac (`#1899`_) + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#1880`_ + - bug + - high + - Wrong Type Hint on some keywords. (`locator: Union[WebElement, None, str]`) + * - `#1824`_ + - enhancement + - high + - Add new selenium 4 print page as PDF functionality + * - `#1894`_ + - enhancement + - high + - Add warning about frame deselection when using `Page Should Contain` keyword. + * - `#1900`_ + - enhancement + - high + - Add driver Service Class into Open Browser + * - `#1826`_ + - --- + - high + - Start Deprecation and Removal of Selenium2Library (deep) references/package + * - `#1899`_ + - --- + - --- + - Make test run on Mac + +Altogether 6 issues. View on the `issue tracker `__. + +.. _#1880: https://github.com/robotframework/SeleniumLibrary/issues/1880 +.. _#1824: https://github.com/robotframework/SeleniumLibrary/issues/1824 +.. _#1894: https://github.com/robotframework/SeleniumLibrary/issues/1894 +.. _#1900: https://github.com/robotframework/SeleniumLibrary/issues/1900 +.. _#1826: https://github.com/robotframework/SeleniumLibrary/issues/1826 +.. _#1899: https://github.com/robotframework/SeleniumLibrary/issues/1899 From 30a62e559b81df1e63d8848b6721e32e5606320d Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Wed, 22 May 2024 08:15:48 -0400 Subject: [PATCH 044/122] Updated version to 6.4.0 --- src/SeleniumLibrary/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index b473aeebc..06b3d79d3 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -52,7 +52,7 @@ from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay -__version__ = "6.4.0rc1" +__version__ = "6.4.0" class SeleniumLibrary(DynamicCore): From e817786570b02ba4f914cd4cfe1c3152ea678bee Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Wed, 22 May 2024 08:16:41 -0400 Subject: [PATCH 045/122] Generated docs for version 6.4.0 --- docs/SeleniumLibrary.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/SeleniumLibrary.html b/docs/SeleniumLibrary.html index 095f88efd..49a9a757a 100644 --- a/docs/SeleniumLibrary.html +++ b/docs/SeleniumLibrary.html @@ -1192,7 +1192,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a + + + + + + + + +
+

Opening library documentation failed

+
    +
  • Verify that you have JavaScript enabled in your browser.
  • +
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • +
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
+
+ + + + + + + + + + + + + + + + From ffd78ec9cc05a03bfc9a96b251f8e3d12913d0a9 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 10 Jun 2024 20:56:34 -0400 Subject: [PATCH 068/122] Regenerated project docs --- docs/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.html b/docs/index.html index 947799b22..9ecf5168b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,8 +29,8 @@

Introduction<

SeleniumLibrary is a web testing library for Robot Framework that utilizes the Selenium tool internally. The project is hosted on GitHub and downloads can be found from PyPI.

-

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 or -newer. In addition to the normal Python interpreter, it works also +

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.11. +In addition to the normal Python interpreter, it works also with PyPy.

SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to Selenium2Library and then later renamed back to SeleniumLibrary. From 0c19d3cbe5124667385a7ef90ffc01c6cd53b5c5 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Wed, 12 Jun 2024 23:26:29 +0300 Subject: [PATCH 069/122] Fix translation cmd Fixes #1908 --- src/SeleniumLibrary/entry/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SeleniumLibrary/entry/__main__.py b/src/SeleniumLibrary/entry/__main__.py index 21a6896e6..e8b886118 100644 --- a/src/SeleniumLibrary/entry/__main__.py +++ b/src/SeleniumLibrary/entry/__main__.py @@ -49,7 +49,7 @@ def cli(): required=True, ) @click.option( - "--plugin", + "--plugins", help="Same as plugins argument in the library import.", default=None, type=str, @@ -97,7 +97,7 @@ def translation( print("Translation is valid, no updated needed.") else: with filename.open("w") as file: - json.dump(translation, file, indent=4) + json.dump(lib_translation, file, indent=4) print(f"Translation file created in {filename.absolute()}") From 6a620262995663136dc5ec1c6715014fb2408536 Mon Sep 17 00:00:00 2001 From: Tatu Aalto Date: Thu, 13 Jun 2024 22:58:07 +0300 Subject: [PATCH 070/122] Add test for entry point --- atest/acceptance/entry_point.robot | 36 ++++++++++++++++++++++ src/SeleniumLibrary/entry/get_versions.py | 2 +- utest/test/translation/test_translation.py | 1 - 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 atest/acceptance/entry_point.robot diff --git a/atest/acceptance/entry_point.robot b/atest/acceptance/entry_point.robot new file mode 100644 index 000000000..a92c3740a --- /dev/null +++ b/atest/acceptance/entry_point.robot @@ -0,0 +1,36 @@ +*** Settings *** +Library Process + + +*** Test Cases *** +Entry Point Version + ${process} = Run Process + ... python -m SeleniumLibrary.entry --version + ... shell=True + ... cwd=${EXECDIR}/src + Log ${process.stdout} + Log ${process.stderr} + Should Be Equal As Integers ${process.rc} 0 + Should Be Empty ${process.stderr} + Should Contain ${process.stdout} Used Python is: + Should Contain ${process.stdout} Installed selenium version is: + +Entry Point Translation + ${process} = Run Process + ... python -m SeleniumLibrary.entry translation ${OUTPUT_DIR}/translation.json + ... shell=True + ... cwd=${EXECDIR}/src + Log ${process.stdout} + Log ${process.stderr} + Should Be Equal As Integers ${process.rc} 0 + Should Be Empty ${process.stderr} + Should Be Equal ${process.stdout} Translation file created in ${OUTPUT_DIR}/translation.json + ${process} = Run Process + ... python -m SeleniumLibrary.entry translation --compare ${OUTPUT_DIR}/translation.json + ... shell=True + ... cwd=${EXECDIR}/src + Log ${process.stdout} + Log ${process.stderr} + Should Be Equal As Integers ${process.rc} 0 + Should Be Empty ${process.stderr} + Should Be Equal ${process.stdout} Translation is valid, no updated needed. diff --git a/src/SeleniumLibrary/entry/get_versions.py b/src/SeleniumLibrary/entry/get_versions.py index 9c78f7d36..51e68da7a 100644 --- a/src/SeleniumLibrary/entry/get_versions.py +++ b/src/SeleniumLibrary/entry/get_versions.py @@ -45,7 +45,7 @@ def get_version(): ) return ( f"\nUsed Python is: {sys.executable}\n\tVersion: {python_version}\n" - f'Robot Framework version: "{get_rf_version()}\n"' + f'Robot Framework version: "{get_rf_version()}"\n' f"Installed SeleniumLibrary version is: {get_library_version()}\n" f"Installed selenium version is: {__version__}\n" ) diff --git a/utest/test/translation/test_translation.py b/utest/test/translation/test_translation.py index c6726a21a..7d1255241 100644 --- a/utest/test/translation/test_translation.py +++ b/utest/test/translation/test_translation.py @@ -8,7 +8,6 @@ @pytest.fixture() def sl() -> SeleniumLibrary: - d = Path(__file__).parent.parent.absolute() sys.path.append(str(Path(__file__).parent.parent.absolute())) return SeleniumLibrary(language="FI") From e64748741018fac420e594c236d5f4f77b7d0e65 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 15:17:01 -0400 Subject: [PATCH 071/122] Corrected spellings of argument --- src/SeleniumLibrary/__init__.py | 4 ++-- src/SeleniumLibrary/entry/__main__.py | 2 +- .../PluginDocumentation.test_many_plugins.approved.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 7e7d07f39..1db59176c 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -578,8 +578,8 @@ class SeleniumLibrary(DynamicCore): Template translation file, with English language can be created by running: `rfselib translation /path/to/translation.json` command. Command does not provide translations to other languages, it only provides easy way to create full list keywords and their documentation in correct - format. It is also possible to add keywords from library plugins by providing `--plugings` arguments - to command. Example: `rfselib translation --plugings myplugin.SomePlugin /path/to/translation.json` The + format. It is also possible to add keywords from library plugins by providing `--plugins` arguments + to command. Example: `rfselib translation --plugins myplugin.SomePlugin /path/to/translation.json` The generated json file contains `sha256` key, which contains the sha256 sum of the library documentation. The sha256 sum is used by `rfselib translation --compare /path/to/translation.json` command, which compares the translation to the library and prints outs a table which tells if there are changes needed for diff --git a/src/SeleniumLibrary/entry/__main__.py b/src/SeleniumLibrary/entry/__main__.py index e8b886118..47e049233 100644 --- a/src/SeleniumLibrary/entry/__main__.py +++ b/src/SeleniumLibrary/entry/__main__.py @@ -75,7 +75,7 @@ def translation( The filename argument will tell where the default json file is saved. - The --plugin argument will add plugin keywords in addition to the library keywords + The --plugins argument will add plugin keywords in addition to the library keywords into the default translation json file. It is used the same as plugins argument in the library import. diff --git a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt index 9d442556c..447445852 100644 --- a/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt +++ b/utest/test/api/approved_files/PluginDocumentation.test_many_plugins.approved.txt @@ -517,8 +517,8 @@ translated, instead ``name`` should be kept the same. Template translation file, with English language can be created by running: `rfselib translation /path/to/translation.json` command. Command does not provide translations to other languages, it only provides easy way to create full list keywords and their documentation in correct -format. It is also possible to add keywords from library plugins by providing `--plugings` arguments -to command. Example: `rfselib translation --plugings myplugin.SomePlugin /path/to/translation.json` The +format. It is also possible to add keywords from library plugins by providing `--plugins` arguments +to command. Example: `rfselib translation --plugins myplugin.SomePlugin /path/to/translation.json` The generated json file contains `sha256` key, which contains the sha256 sum of the library documentation. The sha256 sum is used by `rfselib translation --compare /path/to/translation.json` command, which compares the translation to the library and prints outs a table which tells if there are changes needed for From 8ef19268aed13f3f9621e34781453558a8b0d72d Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 16:13:32 -0400 Subject: [PATCH 072/122] Release notes for 6.5.0 --- docs/SeleniumLibrary-6.5.0.rst | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/SeleniumLibrary-6.5.0.rst diff --git a/docs/SeleniumLibrary-6.5.0.rst b/docs/SeleniumLibrary-6.5.0.rst new file mode 100644 index 000000000..963bd12c6 --- /dev/null +++ b/docs/SeleniumLibrary-6.5.0.rst @@ -0,0 +1,98 @@ +===================== +SeleniumLibrary 6.5.0 +===================== + + +.. default-role:: code + + +SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes +the Selenium_ tool internally. SeleniumLibrary 6.5.0 is a new release with +keyword and keyword documentation translations. + +If you have pip_ installed, just run + +:: + + pip install --upgrade robotframework-seleniumlibrary + +to install the latest available release or use + +:: + + pip install robotframework-seleniumlibrary==6.5.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +SeleniumLibrary 6.5.0 was released on Saturday June 15, 2024. SeleniumLibrary supports +Python 3.8 through 3.11, Selenium 4.20.0 and 4.21.0 and +Robot Framework 5.0.1, 6.1.1 and 7.0. + +.. _Robot Framework: http://robotframework.org +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium: http://seleniumhq.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary +.. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues?q=milestone%3Av6.5.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +- Keyword and keyword documentation translation (`#1908`_) + This brings in the ability to translate both the keyword names as well as + the keyword documentation. Details on how to create translations can be found + within the keyword documentation. In addition the introduction of the selib tool + allows for further enhancement like ``transform`` which will run Robotidy + alongside a (future) SeleniumLibrary transformer to automatically handle keyword + deprecations. + + A sample translation of the SeleniumLibrary keywords and documentation to Finnish + can be found in + `this marketsquare repo `_. + +Acknowledgements +================ + +I want to thank + +- jeromehuewe for noting the unspecified upper supported Python version (`#1903`_) +- `Tatu Aalto `_ for all the work around bringing in + the translation documentation functionality and the selib tool (`#1907`_) + +I also want to thank `Yuri Verweij `_ for his continued +collaboration on maintaining the SeleniumLibrary. + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#1908`_ + - bug + - high + - Fix bug in etry point translation creation + * - `#1903`_ + - enhancement + - medium + - Specify supported Python version + * - `#1907`_ + - enhancement + - medium + - Translation documentation + +Altogether 3 issues. View on the `issue tracker `__. + +.. _#1908: https://github.com/robotframework/SeleniumLibrary/issues/1908 +.. _#1903: https://github.com/robotframework/SeleniumLibrary/issues/1903 +.. _#1907: https://github.com/robotframework/SeleniumLibrary/issues/1907 From a30541338e0b973d5d4a1f38dd58402a0a4f4196 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 16:14:08 -0400 Subject: [PATCH 073/122] Updated version to 6.5.0 --- src/SeleniumLibrary/__init__.py | 1736 +++++++++++++++---------------- 1 file changed, 868 insertions(+), 868 deletions(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 1db59176c..1b2618941 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -1,868 +1,868 @@ -# Copyright 2008-2011 Nokia Networks -# Copyright 2011-2016 Ryan Tomac, Ed Manlove and contributors -# Copyright 2016- Robot Framework Foundation -# -# 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. -from collections import namedtuple -from datetime import timedelta -import importlib -from inspect import getdoc, isclass -from pathlib import Path -import pkgutil -from typing import Optional, List, Union - -from robot.api import logger -from robot.errors import DataError -from robot.libraries.BuiltIn import BuiltIn -from robot.utils import is_string -from robot.utils.importer import Importer - -from robotlibcore import DynamicCore -from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.remote.webelement import WebElement - -from SeleniumLibrary.base import LibraryComponent -from SeleniumLibrary.errors import NoOpenBrowser, PluginError -from SeleniumLibrary.keywords import ( - AlertKeywords, - BrowserManagementKeywords, - CookieKeywords, - ElementKeywords, - ExpectedConditionKeywords, - FormElementKeywords, - FrameKeywords, - JavaScriptKeywords, - RunOnFailureKeywords, - ScreenshotKeywords, - SelectElementKeywords, - TableElementKeywords, - WaitingKeywords, - WebDriverCache, - WindowKeywords, -) -from SeleniumLibrary.keywords.screenshot import EMBED -from SeleniumLibrary.locators import ElementFinder -from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay - - -__version__ = "6.5.0rc1" - - -class SeleniumLibrary(DynamicCore): - """SeleniumLibrary is a web testing library for Robot Framework. - - This document explains how to use keywords provided by SeleniumLibrary. - For information about installation, support, and more, please visit the - [https://github.com/robotframework/SeleniumLibrary|project pages]. - For more information about Robot Framework, see http://robotframework.org. - - SeleniumLibrary uses the Selenium WebDriver modules internally to - control a web browser. See http://seleniumhq.org for more information - about Selenium in general and SeleniumLibrary README.rst - [https://github.com/robotframework/SeleniumLibrary#browser-drivers|Browser drivers chapter] - for more details about WebDriver binary installation. - - %TOC% - - = Locating elements = - - All keywords in SeleniumLibrary that need to interact with an element - on a web page take an argument typically named ``locator`` that specifies - how to find the element. Most often the locator is given as a string - using the locator syntax described below, but `using WebElements` is - possible too. - - == Locator syntax == - - SeleniumLibrary supports finding elements based on different strategies - such as the element id, XPath expressions, or CSS selectors. The strategy - can either be explicitly specified with a prefix or the strategy can be - implicit. - - === Default locator strategy === - - By default, locators are considered to use the keyword specific default - locator strategy. All keywords support finding elements based on ``id`` - and ``name`` attributes, but some keywords support additional attributes - or other values that make sense in their context. For example, `Click - Link` supports the ``href`` attribute and the link text and addition - to the normal ``id`` and ``name``. - - Examples: - - | `Click Element` | example | # Match based on ``id`` or ``name``. | - | `Click Link` | example | # Match also based on link text and ``href``. | - | `Click Button` | example | # Match based on ``id``, ``name`` or ``value``. | - - If a locator accidentally starts with a prefix recognized as `explicit - locator strategy` or `implicit XPath strategy`, it is possible to use - the explicit ``default`` prefix to enable the default strategy. - - Examples: - - | `Click Element` | name:foo | # Find element with name ``foo``. | - | `Click Element` | default:name:foo | # Use default strategy with value ``name:foo``. | - | `Click Element` | //foo | # Find element using XPath ``//foo``. | - | `Click Element` | default: //foo | # Use default strategy with value ``//foo``. | - - === Explicit locator strategy === - - The explicit locator strategy is specified with a prefix using either - syntax ``strategy:value`` or ``strategy=value``. The former syntax - is preferred because the latter is identical to Robot Framework's - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#named-argument-syntax| - named argument syntax] and that can cause problems. Spaces around - the separator are ignored, so ``id:foo``, ``id: foo`` and ``id : foo`` - are all equivalent. - - Locator strategies that are supported by default are listed in the table - below. In addition to them, it is possible to register `custom locators`. - - | = Strategy = | = Match based on = | = Example = | - | id | Element ``id``. | ``id:example`` | - | name | ``name`` attribute. | ``name:example`` | - | identifier | Either ``id`` or ``name``. | ``identifier:example`` | - | class | Element ``class``. | ``class:example`` | - | tag | Tag name. | ``tag:div`` | - | xpath | XPath expression. | ``xpath://div[@id="example"]`` | - | css | CSS selector. | ``css:div#example`` | - | dom | DOM expression. | ``dom:document.images[5]`` | - | link | Exact text a link has. | ``link:The example`` | - | partial link | Partial link text. | ``partial link:he ex`` | - | sizzle | Sizzle selector deprecated. | ``sizzle:div.example`` | - | data | Element ``data-*`` attribute | ``data:id:my_id`` | - | jquery | jQuery expression. | ``jquery:div.example`` | - | default | Keyword specific default behavior. | ``default:example`` | - - See the `Default locator strategy` section below for more information - about how the default strategy works. Using the explicit ``default`` - prefix is only necessary if the locator value itself accidentally - matches some of the explicit strategies. - - Different locator strategies have different pros and cons. Using ids, - either explicitly like ``id:foo`` or by using the `default locator - strategy` simply like ``foo``, is recommended when possible, because - the syntax is simple and locating elements by id is fast for browsers. - If an element does not have an id or the id is not stable, other - solutions need to be used. If an element has a unique tag name or class, - using ``tag``, ``class`` or ``css`` strategy like ``tag:h1``, - ``class:example`` or ``css:h1.example`` is often an easy solution. In - more complex cases using XPath expressions is typically the best - approach. They are very powerful but a downside is that they can also - get complex. - - Examples: - - | `Click Element` | id:foo | # Element with id 'foo'. | - | `Click Element` | css:div#foo h1 | # h1 element under div with id 'foo'. | - | `Click Element` | xpath: //div[@id="foo"]//h1 | # Same as the above using XPath, not CSS. | - | `Click Element` | xpath: //*[contains(text(), "example")] | # Element containing text 'example'. | - - *NOTE:* - - - The ``strategy:value`` syntax is only supported by SeleniumLibrary 3.0 - and newer. - - Using the ``sizzle`` strategy or its alias ``jquery`` requires that - the system under test contains the jQuery library. - - Prior to SeleniumLibrary 3.0, table related keywords only supported - ``xpath``, ``css`` and ``sizzle/jquery`` strategies. - - ``data`` strategy is conveniance locator that will construct xpath from the parameters. - If you have element like `

`, you locate the element via - ``data:automation:automation-id-2``. This feature was added in SeleniumLibrary 5.2.0 - - === Implicit XPath strategy === - - If the locator starts with ``//`` or multiple opening parenthesis in front - of the ``//``, the locator is considered to be an XPath expression. In other - words, using ``//div`` is equivalent to using explicit ``xpath://div`` and - ``((//div))`` is equivalent to using explicit ``xpath:((//div))`` - - Examples: - - | `Click Element` | //div[@id="foo"]//h1 | - | `Click Element` | (//div)[2] | - - The support for the ``(//`` prefix is new in SeleniumLibrary 3.0. - Supporting multiple opening parenthesis is new in SeleniumLibrary 5.0. - - === Chaining locators === - - It is possible chain multiple locators together as single locator. Each chained locator must start with locator - strategy. Chained locators must be separated with single space, two greater than characters and followed with - space. It is also possible mix different locator strategies, example css or xpath. Also a list can also be - used to specify multiple locators. This is useful, is some part of locator would match as the locator separator - but it should not. Or if there is need to existing WebElement as locator. - - Although all locators support chaining, some locator strategies do not abey the chaining. This is because - some locator strategies use JavaScript to find elements and JavaScript is executed for the whole browser context - and not for the element found be the previous locator. Chaining is supported by locator strategies which - are based on Selenium API, like `xpath` or `css`, but example chaining is not supported by `sizzle` or `jquery - - Examples: - | `Click Element` | css:.bar >> xpath://a | # To find a link which is present after an element with class "bar" | - - List examples: - | ${locator_list} = | `Create List` | css:div#div_id | xpath://*[text(), " >> "] | - | `Page Should Contain Element` | ${locator_list} | | | - | ${element} = | Get WebElement | xpath://*[text(), " >> "] | | - | ${locator_list} = | `Create List` | css:div#div_id | ${element} | - | `Page Should Contain Element` | ${locator_list} | | | - - Chaining locators in new in SeleniumLibrary 5.0 - - == Using WebElements == - - In addition to specifying a locator as a string, it is possible to use - Selenium's WebElement objects. This requires first getting a WebElement, - for example, by using the `Get WebElement` keyword. - - | ${elem} = | `Get WebElement` | id:example | - | `Click Element` | ${elem} | | - - == Custom locators == - - If more complex lookups are required than what is provided through the - default locators, custom lookup strategies can be created. Using custom - locators is a two part process. First, create a keyword that returns - a WebElement that should be acted on: - - | Custom Locator Strategy | [Arguments] | ${browser} | ${locator} | ${tag} | ${constraints} | - | | ${element}= | Execute Javascript | return window.document.getElementById('${locator}'); | - | | [Return] | ${element} | - - This keyword is a reimplementation of the basic functionality of the - ``id`` locator where ``${browser}`` is a reference to a WebDriver - instance and ``${locator}`` is the name of the locator strategy. To use - this locator, it must first be registered by using the - `Add Location Strategy` keyword: - - | `Add Location Strategy` | custom | Custom Locator Strategy | - - The first argument of `Add Location Strategy` specifies the name of - the strategy and it must be unique. After registering the strategy, - the usage is the same as with other locators: - - | `Click Element` | custom:example | - - See the `Add Location Strategy` keyword for more details. - - = Browser and Window = - - There is different conceptual meaning when SeleniumLibrary talks - about windows or browsers. This chapter explains those differences. - - == Browser == - - When `Open Browser` or `Create WebDriver` keyword is called, it - will create a new Selenium WebDriver instance by using the - [https://www.seleniumhq.org/docs/03_webdriver.jsp|Selenium WebDriver] - API. In SeleniumLibrary terms, a new browser is created. It is - possible to start multiple independent browsers (Selenium Webdriver - instances) at the same time, by calling `Open Browser` or - `Create WebDriver` multiple times. These browsers are usually - independent of each other and do not share data like cookies, - sessions or profiles. Typically when the browser starts, it - creates a single window which is shown to the user. - - == Window == - - Windows are the part of a browser that loads the web site and presents - it to the user. All content of the site is the content of the window. - Windows are children of a browser. In SeleniumLibrary browser is a - synonym for WebDriver instance. One browser may have multiple - windows. Windows can appear as tabs, as separate windows or pop-ups with - different position and size. Windows belonging to the same browser - typically share the sessions detail, like cookies. If there is a - need to separate sessions detail, example login with two different - users, two browsers (Selenium WebDriver instances) must be created. - New windows can be opened example by the application under test or - by example `Execute Javascript` keyword: - - | `Execute Javascript` window.open() # Opens a new window with location about:blank - - The example below opens multiple browsers and windows, - to demonstrate how the different keywords can be used to interact - with browsers, and windows attached to these browsers. - - Structure: - | BrowserA - | Window 1 (location=https://robotframework.org/) - | Window 2 (location=https://robocon.io/) - | Window 3 (location=https://github.com/robotframework/) - | - | BrowserB - | Window 1 (location=https://github.com/) - - Example: - | `Open Browser` | https://robotframework.org | ${BROWSER} | alias=BrowserA | # BrowserA with first window is opened. | - | `Execute Javascript` | window.open() | | | # In BrowserA second window is opened. | - | `Switch Window` | locator=NEW | | | # Switched to second window in BrowserA | - | `Go To` | https://robocon.io | | | # Second window navigates to robocon site. | - | `Execute Javascript` | window.open() | | | # In BrowserA third window is opened. | - | ${handle} | `Switch Window` | locator=NEW | | # Switched to third window in BrowserA | - | `Go To` | https://github.com/robotframework/ | | | # Third windows goes to robot framework github site. | - | `Open Browser` | https://github.com | ${BROWSER} | alias=BrowserB | # BrowserB with first windows is opened. | - | ${location} | `Get Location` | | | # ${location} is: https://www.github.com | - | `Switch Window` | ${handle} | browser=BrowserA | | # BrowserA second windows is selected. | - | ${location} | `Get Location` | | | # ${location} = https://robocon.io/ | - | @{locations 1} | `Get Locations` | | | # By default, lists locations under the currectly active browser (BrowserA). | - | @{locations 2} | `Get Locations` | browser=ALL | | # By using browser=ALL argument keyword list all locations from all browsers. | - - The above example, @{locations 1} contains the following items: - https://robotframework.org/, https://robocon.io/ and - https://github.com/robotframework/'. The @{locations 2} - contains the following items: https://robotframework.org/, - https://robocon.io/, https://github.com/robotframework/' - and 'https://github.com/. - - = Browser and Driver options and service class = - - This section talks about how to configure either the browser or - the driver using the options and service arguments of the `Open - Browser` keyword. - - == Configuring the browser using the Selenium Options == - - As noted within the keyword documentation for `Open Browser`, its - ``options`` argument accepts Selenium options in two different - formats: as a string and as Python object which is an instance of - the Selenium options class. - - === Options string format === - - The string format allows defining Selenium options methods - or attributes and their arguments in Robot Framework test data. - The method and attributes names are case and space sensitive and - must match to the Selenium options methods and attributes names. - When defining a method, it must be defined in a similar way as in - python: method name, opening parenthesis, zero to many arguments - and closing parenthesis. If there is a need to define multiple - arguments for a single method, arguments must be separated with - comma, just like in Python. Example: `add_argument("--headless")` - or `add_experimental_option("key", "value")`. Attributes are - defined in a similar way as in Python: attribute name, equal sign, - and attribute value. Example, `headless=True`. Multiple methods - and attributes must be separated by a semicolon. Example: - `add_argument("--headless");add_argument("--start-maximized")`. - - Arguments allow defining Python data types and arguments are - evaluated by using Python - [https://docs.python.org/3/library/ast.html#ast.literal_eval|ast.literal_eval]. - Strings must be quoted with single or double quotes, example "value" - or 'value'. It is also possible to define other Python builtin - data types, example `True` or `None`, by not using quotes - around the arguments. - - The string format is space friendly. Usually, spaces do not alter - the defining methods or attributes. There are two exceptions. - In some Robot Framework test data formats, two or more spaces are - considered as cell separator and instead of defining a single - argument, two or more arguments may be defined. Spaces in string - arguments are not removed and are left as is. Example - `add_argument ( "--headless" )` is same as - `add_argument("--headless")`. But `add_argument(" --headless ")` is - not same same as `add_argument ( "--headless" )`, because - spaces inside of quotes are not removed. Please note that if - options string contains backslash, example a Windows OS path, - the backslash needs escaping both in Robot Framework data and - in Python side. This means single backslash must be writen using - four backslash characters. Example, Windows path: - "C:\\path\\to\\profile" must be written as - "C:\\\\\\\\path\\\\\\to\\\\\\\\profile". Another way to write - backslash is use Python - [https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals|raw strings] - and example write: r"C:\\\\path\\\\to\\\\profile". - - === Selenium Options as Python class === - - As last format, ``options`` argument also supports receiving - the Selenium options as Python class instance. In this case, the - instance is used as-is and the SeleniumLibrary will not convert - the instance to other formats. - For example, if the following code return value is saved to - `${options}` variable in the Robot Framework data: - | options = webdriver.ChromeOptions() - | options.add_argument('--disable-dev-shm-usage') - | return options - - Then the `${options}` variable can be used as an argument to - ``options``. - - Example the ``options`` argument can be used to launch Chomium-based - applications which utilize the - [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] - . To launch Chromium-based application, use ``options`` to define - `binary_location` attribute and use `add_argument` method to define - `remote-debugging-port` port for the application. Once the browser - is opened, the test can interact with the embedded web-content of - the system under test. - - == Configuring the driver using the Service class == - - With the ``service`` argument, one can setup and configure the driver. For example - one can set the driver location and/port or specify the command line arguments. There - are several browser specific attributes related to logging as well. For the various - Service Class attributes refer to - [https://www.selenium.dev/documentation/webdriver/drivers/service/|the Selenium documentation] - . Currently the ``service`` argument only accepts Selenium service in the string format. - - === Service string format === - - The string format allows for defining Selenium service attributes - and their values in the `Open Browser` keyword. The attributes names - are case and space sensitive and must match to the Selenium attributes - names. Attributes are defined in a similar way as in Python: attribute - name, equal sign, and attribute value. Example, `port=1234`. Multiple - attributes must be separated by a semicolon. Example: - `executable_path='/path/to/driver';port=1234`. Don't have duplicate - attributes, like `service_args=['--append-log', '--readable-timestamp']; - service_args=['--log-level=DEBUG']` as the second will override the first. - Instead combine them as in - `service_args=['--append-log', '--readable-timestamp', '--log-level=DEBUG']` - - Arguments allow defining Python data types and arguments are - evaluated by using Python. Strings must be quoted with single - or double quotes, example "value" or 'value' - - = Timeouts, waits, and delays = - - This section discusses different ways how to wait for elements to - appear on web pages and to slow down execution speed otherwise. - It also explains the `time format` that can be used when setting various - timeouts, waits, and delays. - - == Timeout == - - SeleniumLibrary contains various keywords that have an optional - ``timeout`` argument that specifies how long these keywords should - wait for certain events or actions. These keywords include, for example, - ``Wait ...`` keywords and keywords related to alerts. Additionally - `Execute Async Javascript`. Although it does not have ``timeout``, - argument, uses a timeout to define how long asynchronous JavaScript - can run. - - The default timeout these keywords use can be set globally either by - using the `Set Selenium Timeout` keyword or with the ``timeout`` argument - when `importing` the library. If no default timeout is set globally, the - default is 5 seconds. If None is specified for the timeout argument in the - keywords, the default is used. See `time format` below for supported - timeout syntax. - - == Implicit wait == - - Implicit wait specifies the maximum time how long Selenium waits when - searching for elements. It can be set by using the `Set Selenium Implicit - Wait` keyword or with the ``implicit_wait`` argument when `importing` - the library. See [https://www.seleniumhq.org/docs/04_webdriver_advanced.jsp| - Selenium documentation] for more information about this functionality. - - See `time format` below for supported syntax. - - == Page load == - Page load timeout is the amount of time to wait for page load to complete - until a timeout exception is raised. - - The default page load timeout can be set globally - when `importing` the library with the ``page_load_timeout`` argument - or by using the `Set Selenium Page Load Timeout` keyword. - - See `time format` below for supported timeout syntax. - - Support for page load is new in SeleniumLibrary 6.1 - - == Selenium speed == - - Selenium execution speed can be slowed down globally by using `Set - Selenium speed` keyword. This functionality is designed to be used for - demonstrating or debugging purposes. Using it to make sure that elements - appear on a page is not a good idea. The above-explained timeouts - and waits should be used instead. - - See `time format` below for supported syntax. - - == Time format == - - All timeouts and waits can be given as numbers considered seconds - (e.g. ``0.5`` or ``42``) or in Robot Framework's time syntax - (e.g. ``1.5 seconds`` or ``1 min 30 s``). For more information about - the time syntax see the - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format|Robot Framework User Guide]. - - = Run-on-failure functionality = - - SeleniumLibrary has a handy feature that it can automatically execute - a keyword if any of its own keywords fails. By default, it uses the - `Capture Page Screenshot` keyword, but this can be changed either by - using the `Register Keyword To Run On Failure` keyword or with the - ``run_on_failure`` argument when `importing` the library. It is - possible to use any keyword from any imported library or resource file. - - The run-on-failure functionality can be disabled by using a special value - ``NOTHING`` or anything considered false (see `Boolean arguments`) - such as ``NONE``. - - = Boolean arguments = - - Starting from 5.0 SeleniumLibrary relies on Robot Framework to perform the - boolean conversion based on keyword arguments [https://docs.python.org/3/library/typing.html|type hint]. - More details in Robot Framework - [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#supported-conversions|user guide] - - Please note SeleniumLibrary 3 and 4 did have own custom methods to covert - arguments to boolean values. - - = EventFiringWebDriver = - - 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.rst#EventFiringWebDriver|EventFiringWebDriver support] - documentation for further details. - - EventFiringWebDriver is new in SeleniumLibrary 4.0 - - = Thread support = - - SeleniumLibrary is not thread-safe. This is mainly due because the underlying - [https://github.com/SeleniumHQ/selenium/wiki/Frequently-Asked-Questions#q-is-webdriver-thread-safe| - Selenium tool is not thread-safe] within one browser/driver instance. - Because of the limitation in the Selenium side, the keywords or the - API provided by the SeleniumLibrary is not thread-safe. - - = Plugins = - - SeleniumLibrary offers plugins as a way to modify and add library keywords and modify some of the internal - functionality without creating a new library or hacking the source code. See - [https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending.rst#Plugins|plugin API] - documentation for further details. - - Plugin API is new SeleniumLibrary 4.0 - - = Language = - - SeleniumLibrary offers the possibility to translate keyword names and documentation to new language. If language - is defined, SeleniumLibrary will search from - [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#module-search-path | module search path] - for Python packages starting with `robotframework-seleniumlibrary-translation` by using the - [https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/ | Python pluging API]. The Library - is using naming convention to find Python plugins. - - The package must implement a single API call, ``get_language`` without any arguments. The method must return a - dictionary containing two keys: ``language`` and ``path``. The language key value defines which language - the package contains. Also the value should match (case insensitive) the library ``language`` import parameter. - The path parameter value should be full path to the translation file. - - == Translation file == - - The file name or extension is not important, but data must be in [https://www.json.org/json-en.html | json] - format. The keys of json are the methods names, not the keyword names, which implements keywords. Value of - key is json object which contains two keys: ``name`` and ``doc``. The ``name`` key contains the keyword - translated name and `doc` contains translated documentation. Providing doc and name are optional, example - translation json file can only provide translations to keyword names or only to documentation. But it is - always recommended to provide translation to both name and doc. Special key ``__intro__`` is for class level - documentation and ``__init__`` is for init level documentation. These special values ``name`` can not be - translated, instead ``name`` should be kept the same. - - == Generating template translation file == - - Template translation file, with English language can be created by running: - `rfselib translation /path/to/translation.json` command. Command does not provide translations to other - languages, it only provides easy way to create full list keywords and their documentation in correct - format. It is also possible to add keywords from library plugins by providing `--plugins` arguments - to command. Example: `rfselib translation --plugins myplugin.SomePlugin /path/to/translation.json` The - generated json file contains `sha256` key, which contains the sha256 sum of the library documentation. - The sha256 sum is used by `rfselib translation --compare /path/to/translation.json` command, which compares - the translation to the library and prints outs a table which tells if there are changes needed for - the translation file. - - Example project for translation can be found from - [https://github.com/MarketSquare/robotframework-seleniumlibrary-translation-fi | robotframework-seleniumlibrary-translation-fi] - repository. - """ - - ROBOT_LIBRARY_SCOPE = "GLOBAL" - ROBOT_LIBRARY_VERSION = __version__ - - def __init__( - self, - timeout=timedelta(seconds=5), - implicit_wait=timedelta(seconds=0), - run_on_failure="Capture Page Screenshot", - screenshot_root_directory: Optional[str] = None, - plugins: Optional[str] = None, - event_firing_webdriver: Optional[str] = None, - page_load_timeout=timedelta(minutes=5), - action_chain_delay=timedelta(seconds=0.25), - language: Optional[str] = None, - ): - """SeleniumLibrary can be imported with several optional arguments. - - - ``timeout``: - Default value for `timeouts` used with ``Wait ...`` keywords. - - ``implicit_wait``: - Default value for `implicit wait` used when locating elements. - - ``run_on_failure``: - Default action for the `run-on-failure functionality`. - - ``screenshot_root_directory``: - Path to folder where possible screenshots are created or EMBED. - See `Set Screenshot Directory` keyword for further details about EMBED. - If not given, the directory where the log file is written is used. - - ``plugins``: - Allows extending the SeleniumLibrary with external Python classes. - - ``event_firing_webdriver``: - Class for wrapping Selenium with - [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] - - ``page_load_timeout``: - Default value to wait for page load to complete until a timeout exception is raised. - - ``action_chain_delay``: - Default value for `ActionChains` delay to wait in between actions. - - ``language``: - Defines language which is used to translate keyword names and documentation. - """ - self.timeout = _convert_timeout(timeout) - self.implicit_wait = _convert_timeout(implicit_wait) - self.action_chain_delay = _convert_delay(action_chain_delay) - self.page_load_timeout = _convert_timeout(page_load_timeout) - self.speed = 0.0 - self.run_on_failure_keyword = RunOnFailureKeywords.resolve_keyword( - run_on_failure - ) - self._running_on_failure_keyword = False - self.screenshot_root_directory = screenshot_root_directory - self._resolve_screenshot_root_directory() - self._element_finder = ElementFinder(self) - self._plugin_keywords = [] - libraries = [ - AlertKeywords(self), - BrowserManagementKeywords(self), - CookieKeywords(self), - ElementKeywords(self), - ExpectedConditionKeywords(self), - FormElementKeywords(self), - FrameKeywords(self), - JavaScriptKeywords(self), - RunOnFailureKeywords(self), - ScreenshotKeywords(self), - SelectElementKeywords(self), - TableElementKeywords(self), - WaitingKeywords(self), - WindowKeywords(self), - ] - self.ROBOT_LIBRARY_LISTENER = LibraryListener() - self._running_keyword = None - self.event_firing_webdriver = None - if is_truthy(event_firing_webdriver): - self.event_firing_webdriver = self._parse_listener(event_firing_webdriver) - self._plugins = [] - if is_truthy(plugins): - plugin_libs = self._parse_plugins(plugins) - self._plugins = plugin_libs - libraries = libraries + plugin_libs - self._drivers = WebDriverCache() - translation_file = self._get_translation(language) - DynamicCore.__init__(self, libraries, translation_file) - - def run_keyword(self, name: str, args: tuple, kwargs: dict): - try: - return DynamicCore.run_keyword(self, name, args, kwargs) - except Exception: - self.failure_occurred() - raise - - def get_keyword_tags(self, name: str) -> list: - tags = list(DynamicCore.get_keyword_tags(self, name)) - if name in self._plugin_keywords: - tags.append("plugin") - return tags - - def get_keyword_documentation(self, name: str) -> str: - if name == "__intro__": - return self._get_intro_documentation() - return DynamicCore.get_keyword_documentation(self, name) - - def _parse_plugin_doc(self): - Doc = namedtuple("Doc", "doc, name") - for plugin in self._plugins: - yield Doc( - doc=getdoc(plugin) or "No plugin documentation found.", - name=plugin.__class__.__name__, - ) - - def _get_intro_documentation(self): - intro = DynamicCore.get_keyword_documentation(self, "__intro__") - for plugin_doc in self._parse_plugin_doc(): - intro = f"{intro}\n\n" - intro = f"{intro}= Plugin: {plugin_doc.name} =\n\n" - intro = f"{intro}{plugin_doc.doc}" - return intro - - def register_driver(self, driver: WebDriver, alias: str): - """Add's a `driver` to the library WebDriverCache. - - :param driver: Instance of the Selenium `WebDriver`. - :type driver: selenium.webdriver.remote.webdriver.WebDriver - :param alias: Alias given for this `WebDriver` instance. - :type alias: str - :return: The index of the `WebDriver` instance. - :rtype: int - """ - return self._drivers.register(driver, alias) - - def failure_occurred(self): - """Method that is executed when a SeleniumLibrary keyword fails. - - By default, executes the registered run-on-failure keyword. - Libraries extending SeleniumLibrary can overwrite this hook - method if they want to provide custom functionality instead. - """ - if self._running_on_failure_keyword or not self.run_on_failure_keyword: - return - try: - self._running_on_failure_keyword = True - if self.run_on_failure_keyword.lower() == "capture page screenshot": - self.capture_page_screenshot() - else: - BuiltIn().run_keyword(self.run_on_failure_keyword) - except Exception as err: - logger.warn( - f"Keyword '{self.run_on_failure_keyword}' could not be run on failure: {err}" - ) - finally: - self._running_on_failure_keyword = False - - @property - def driver(self) -> WebDriver: - """Current active driver. - - :rtype: selenium.webdriver.remote.webdriver.WebDriver - :raises SeleniumLibrary.errors.NoOpenBrowser: If browser is not open. - """ - if not self._drivers.current: - raise NoOpenBrowser("No browser is open.") - return self._drivers.current - - def find_element( - self, locator: str, parent: Optional[WebElement] = None - ) -> WebElement: - """Find element matching `locator`. - - :param locator: Locator to use when searching the element. - See library documentation for the supported locator syntax. - :type locator: str or selenium.webdriver.remote.webelement.WebElement - :param parent: Optional parent `WebElememt` to search child elements - from. By default, search starts from the root using `WebDriver`. - :type parent: selenium.webdriver.remote.webelement.WebElement - :return: Found `WebElement`. - :rtype: selenium.webdriver.remote.webelement.WebElement - :raises SeleniumLibrary.errors.ElementNotFound: If element not found. - """ - return self._element_finder.find(locator, parent=parent) - - def find_elements( - self, locator: str, parent: WebElement = None - ) -> List[WebElement]: - """Find all elements matching `locator`. - - :param locator: Locator to use when searching the element. - See library documentation for the supported locator syntax. - :type locator: str or selenium.webdriver.remote.webelement.WebElement - :param parent: Optional parent `WebElememt` to search child elements - from. By default, search starts from the root using `WebDriver`. - :type parent: selenium.webdriver.remote.webelement.WebElement - :return: list of found `WebElement` or e,mpty if elements are not found. - :rtype: list[selenium.webdriver.remote.webelement.WebElement] - """ - return self._element_finder.find( - locator, first_only=False, required=False, parent=parent - ) - - def _parse_plugins(self, plugins): - libraries = [] - importer = Importer("test library") - for parsed_plugin in self._string_to_modules(plugins): - plugin = importer.import_class_or_module(parsed_plugin.module) - if not isclass(plugin): - message = f"Importing test library: '{parsed_plugin.module}' failed." - raise DataError(message) - plugin = plugin(self, *parsed_plugin.args, **parsed_plugin.kw_args) - if not isinstance(plugin, LibraryComponent): - message = ( - "Plugin does not inherit SeleniumLibrary.base.LibraryComponent" - ) - raise PluginError(message) - self._store_plugin_keywords(plugin) - libraries.append(plugin) - return libraries - - def _parse_listener(self, event_firing_webdriver): - listener_module = self._string_to_modules(event_firing_webdriver) - listener_count = len(listener_module) - if listener_count > 1: - message = f"Is is possible import only one listener but there was {listener_count} listeners." - raise ValueError(message) - listener_module = listener_module[0] - importer = Importer("test library") - listener = importer.import_class_or_module(listener_module.module) - if not isclass(listener): - message = f"Importing test Selenium lister class '{listener_module.module}' failed." - raise DataError(message) - return listener - - def _string_to_modules(self, modules): - Module = namedtuple("Module", "module, args, kw_args") - parsed_modules = [] - for module in modules.split(","): - module = module.strip() - module_and_args = module.split(";") - module_name = module_and_args.pop(0) - kw_args = {} - args = [] - for argument in module_and_args: - if "=" in argument: - key, value = argument.split("=") - kw_args[key] = value - else: - args.append(argument) - module = Module(module=module_name, args=args, kw_args=kw_args) - parsed_modules.append(module) - return parsed_modules - - def _store_plugin_keywords(self, plugin): - dynamic_core = DynamicCore([plugin]) - self._plugin_keywords.extend(dynamic_core.get_keyword_names()) - - def _resolve_screenshot_root_directory(self): - screenshot_root_directory = self.screenshot_root_directory - if is_string(screenshot_root_directory): - if screenshot_root_directory.upper() == EMBED: - self.screenshot_root_directory = EMBED - - @staticmethod - def _get_translation(language: Union[str, None]) -> Union[Path, None]: - if not language: - return None - discovered_plugins = { - name: importlib.import_module(name) - for _, name, _ in pkgutil.iter_modules() - if name.startswith("robotframework_seleniumlibrary_translation") - } - for plugin in discovered_plugins.values(): - try: - data = plugin.get_language() - except AttributeError: - continue - if data.get("language", "").lower() == language.lower() and data.get( - "path" - ): - return Path(data.get("path")).absolute() - return None +# Copyright 2008-2011 Nokia Networks +# Copyright 2011-2016 Ryan Tomac, Ed Manlove and contributors +# Copyright 2016- Robot Framework Foundation +# +# 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. +from collections import namedtuple +from datetime import timedelta +import importlib +from inspect import getdoc, isclass +from pathlib import Path +import pkgutil +from typing import Optional, List, Union + +from robot.api import logger +from robot.errors import DataError +from robot.libraries.BuiltIn import BuiltIn +from robot.utils import is_string +from robot.utils.importer import Importer + +from robotlibcore import DynamicCore +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement + +from SeleniumLibrary.base import LibraryComponent +from SeleniumLibrary.errors import NoOpenBrowser, PluginError +from SeleniumLibrary.keywords import ( + AlertKeywords, + BrowserManagementKeywords, + CookieKeywords, + ElementKeywords, + ExpectedConditionKeywords, + FormElementKeywords, + FrameKeywords, + JavaScriptKeywords, + RunOnFailureKeywords, + ScreenshotKeywords, + SelectElementKeywords, + TableElementKeywords, + WaitingKeywords, + WebDriverCache, + WindowKeywords, +) +from SeleniumLibrary.keywords.screenshot import EMBED +from SeleniumLibrary.locators import ElementFinder +from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay + + +__version__ = "6.5.0" + + +class SeleniumLibrary(DynamicCore): + """SeleniumLibrary is a web testing library for Robot Framework. + + This document explains how to use keywords provided by SeleniumLibrary. + For information about installation, support, and more, please visit the + [https://github.com/robotframework/SeleniumLibrary|project pages]. + For more information about Robot Framework, see http://robotframework.org. + + SeleniumLibrary uses the Selenium WebDriver modules internally to + control a web browser. See http://seleniumhq.org for more information + about Selenium in general and SeleniumLibrary README.rst + [https://github.com/robotframework/SeleniumLibrary#browser-drivers|Browser drivers chapter] + for more details about WebDriver binary installation. + + %TOC% + + = Locating elements = + + All keywords in SeleniumLibrary that need to interact with an element + on a web page take an argument typically named ``locator`` that specifies + how to find the element. Most often the locator is given as a string + using the locator syntax described below, but `using WebElements` is + possible too. + + == Locator syntax == + + SeleniumLibrary supports finding elements based on different strategies + such as the element id, XPath expressions, or CSS selectors. The strategy + can either be explicitly specified with a prefix or the strategy can be + implicit. + + === Default locator strategy === + + By default, locators are considered to use the keyword specific default + locator strategy. All keywords support finding elements based on ``id`` + and ``name`` attributes, but some keywords support additional attributes + or other values that make sense in their context. For example, `Click + Link` supports the ``href`` attribute and the link text and addition + to the normal ``id`` and ``name``. + + Examples: + + | `Click Element` | example | # Match based on ``id`` or ``name``. | + | `Click Link` | example | # Match also based on link text and ``href``. | + | `Click Button` | example | # Match based on ``id``, ``name`` or ``value``. | + + If a locator accidentally starts with a prefix recognized as `explicit + locator strategy` or `implicit XPath strategy`, it is possible to use + the explicit ``default`` prefix to enable the default strategy. + + Examples: + + | `Click Element` | name:foo | # Find element with name ``foo``. | + | `Click Element` | default:name:foo | # Use default strategy with value ``name:foo``. | + | `Click Element` | //foo | # Find element using XPath ``//foo``. | + | `Click Element` | default: //foo | # Use default strategy with value ``//foo``. | + + === Explicit locator strategy === + + The explicit locator strategy is specified with a prefix using either + syntax ``strategy:value`` or ``strategy=value``. The former syntax + is preferred because the latter is identical to Robot Framework's + [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#named-argument-syntax| + named argument syntax] and that can cause problems. Spaces around + the separator are ignored, so ``id:foo``, ``id: foo`` and ``id : foo`` + are all equivalent. + + Locator strategies that are supported by default are listed in the table + below. In addition to them, it is possible to register `custom locators`. + + | = Strategy = | = Match based on = | = Example = | + | id | Element ``id``. | ``id:example`` | + | name | ``name`` attribute. | ``name:example`` | + | identifier | Either ``id`` or ``name``. | ``identifier:example`` | + | class | Element ``class``. | ``class:example`` | + | tag | Tag name. | ``tag:div`` | + | xpath | XPath expression. | ``xpath://div[@id="example"]`` | + | css | CSS selector. | ``css:div#example`` | + | dom | DOM expression. | ``dom:document.images[5]`` | + | link | Exact text a link has. | ``link:The example`` | + | partial link | Partial link text. | ``partial link:he ex`` | + | sizzle | Sizzle selector deprecated. | ``sizzle:div.example`` | + | data | Element ``data-*`` attribute | ``data:id:my_id`` | + | jquery | jQuery expression. | ``jquery:div.example`` | + | default | Keyword specific default behavior. | ``default:example`` | + + See the `Default locator strategy` section below for more information + about how the default strategy works. Using the explicit ``default`` + prefix is only necessary if the locator value itself accidentally + matches some of the explicit strategies. + + Different locator strategies have different pros and cons. Using ids, + either explicitly like ``id:foo`` or by using the `default locator + strategy` simply like ``foo``, is recommended when possible, because + the syntax is simple and locating elements by id is fast for browsers. + If an element does not have an id or the id is not stable, other + solutions need to be used. If an element has a unique tag name or class, + using ``tag``, ``class`` or ``css`` strategy like ``tag:h1``, + ``class:example`` or ``css:h1.example`` is often an easy solution. In + more complex cases using XPath expressions is typically the best + approach. They are very powerful but a downside is that they can also + get complex. + + Examples: + + | `Click Element` | id:foo | # Element with id 'foo'. | + | `Click Element` | css:div#foo h1 | # h1 element under div with id 'foo'. | + | `Click Element` | xpath: //div[@id="foo"]//h1 | # Same as the above using XPath, not CSS. | + | `Click Element` | xpath: //*[contains(text(), "example")] | # Element containing text 'example'. | + + *NOTE:* + + - The ``strategy:value`` syntax is only supported by SeleniumLibrary 3.0 + and newer. + - Using the ``sizzle`` strategy or its alias ``jquery`` requires that + the system under test contains the jQuery library. + - Prior to SeleniumLibrary 3.0, table related keywords only supported + ``xpath``, ``css`` and ``sizzle/jquery`` strategies. + - ``data`` strategy is conveniance locator that will construct xpath from the parameters. + If you have element like `
`, you locate the element via + ``data:automation:automation-id-2``. This feature was added in SeleniumLibrary 5.2.0 + + === Implicit XPath strategy === + + If the locator starts with ``//`` or multiple opening parenthesis in front + of the ``//``, the locator is considered to be an XPath expression. In other + words, using ``//div`` is equivalent to using explicit ``xpath://div`` and + ``((//div))`` is equivalent to using explicit ``xpath:((//div))`` + + Examples: + + | `Click Element` | //div[@id="foo"]//h1 | + | `Click Element` | (//div)[2] | + + The support for the ``(//`` prefix is new in SeleniumLibrary 3.0. + Supporting multiple opening parenthesis is new in SeleniumLibrary 5.0. + + === Chaining locators === + + It is possible chain multiple locators together as single locator. Each chained locator must start with locator + strategy. Chained locators must be separated with single space, two greater than characters and followed with + space. It is also possible mix different locator strategies, example css or xpath. Also a list can also be + used to specify multiple locators. This is useful, is some part of locator would match as the locator separator + but it should not. Or if there is need to existing WebElement as locator. + + Although all locators support chaining, some locator strategies do not abey the chaining. This is because + some locator strategies use JavaScript to find elements and JavaScript is executed for the whole browser context + and not for the element found be the previous locator. Chaining is supported by locator strategies which + are based on Selenium API, like `xpath` or `css`, but example chaining is not supported by `sizzle` or `jquery + + Examples: + | `Click Element` | css:.bar >> xpath://a | # To find a link which is present after an element with class "bar" | + + List examples: + | ${locator_list} = | `Create List` | css:div#div_id | xpath://*[text(), " >> "] | + | `Page Should Contain Element` | ${locator_list} | | | + | ${element} = | Get WebElement | xpath://*[text(), " >> "] | | + | ${locator_list} = | `Create List` | css:div#div_id | ${element} | + | `Page Should Contain Element` | ${locator_list} | | | + + Chaining locators in new in SeleniumLibrary 5.0 + + == Using WebElements == + + In addition to specifying a locator as a string, it is possible to use + Selenium's WebElement objects. This requires first getting a WebElement, + for example, by using the `Get WebElement` keyword. + + | ${elem} = | `Get WebElement` | id:example | + | `Click Element` | ${elem} | | + + == Custom locators == + + If more complex lookups are required than what is provided through the + default locators, custom lookup strategies can be created. Using custom + locators is a two part process. First, create a keyword that returns + a WebElement that should be acted on: + + | Custom Locator Strategy | [Arguments] | ${browser} | ${locator} | ${tag} | ${constraints} | + | | ${element}= | Execute Javascript | return window.document.getElementById('${locator}'); | + | | [Return] | ${element} | + + This keyword is a reimplementation of the basic functionality of the + ``id`` locator where ``${browser}`` is a reference to a WebDriver + instance and ``${locator}`` is the name of the locator strategy. To use + this locator, it must first be registered by using the + `Add Location Strategy` keyword: + + | `Add Location Strategy` | custom | Custom Locator Strategy | + + The first argument of `Add Location Strategy` specifies the name of + the strategy and it must be unique. After registering the strategy, + the usage is the same as with other locators: + + | `Click Element` | custom:example | + + See the `Add Location Strategy` keyword for more details. + + = Browser and Window = + + There is different conceptual meaning when SeleniumLibrary talks + about windows or browsers. This chapter explains those differences. + + == Browser == + + When `Open Browser` or `Create WebDriver` keyword is called, it + will create a new Selenium WebDriver instance by using the + [https://www.seleniumhq.org/docs/03_webdriver.jsp|Selenium WebDriver] + API. In SeleniumLibrary terms, a new browser is created. It is + possible to start multiple independent browsers (Selenium Webdriver + instances) at the same time, by calling `Open Browser` or + `Create WebDriver` multiple times. These browsers are usually + independent of each other and do not share data like cookies, + sessions or profiles. Typically when the browser starts, it + creates a single window which is shown to the user. + + == Window == + + Windows are the part of a browser that loads the web site and presents + it to the user. All content of the site is the content of the window. + Windows are children of a browser. In SeleniumLibrary browser is a + synonym for WebDriver instance. One browser may have multiple + windows. Windows can appear as tabs, as separate windows or pop-ups with + different position and size. Windows belonging to the same browser + typically share the sessions detail, like cookies. If there is a + need to separate sessions detail, example login with two different + users, two browsers (Selenium WebDriver instances) must be created. + New windows can be opened example by the application under test or + by example `Execute Javascript` keyword: + + | `Execute Javascript` window.open() # Opens a new window with location about:blank + + The example below opens multiple browsers and windows, + to demonstrate how the different keywords can be used to interact + with browsers, and windows attached to these browsers. + + Structure: + | BrowserA + | Window 1 (location=https://robotframework.org/) + | Window 2 (location=https://robocon.io/) + | Window 3 (location=https://github.com/robotframework/) + | + | BrowserB + | Window 1 (location=https://github.com/) + + Example: + | `Open Browser` | https://robotframework.org | ${BROWSER} | alias=BrowserA | # BrowserA with first window is opened. | + | `Execute Javascript` | window.open() | | | # In BrowserA second window is opened. | + | `Switch Window` | locator=NEW | | | # Switched to second window in BrowserA | + | `Go To` | https://robocon.io | | | # Second window navigates to robocon site. | + | `Execute Javascript` | window.open() | | | # In BrowserA third window is opened. | + | ${handle} | `Switch Window` | locator=NEW | | # Switched to third window in BrowserA | + | `Go To` | https://github.com/robotframework/ | | | # Third windows goes to robot framework github site. | + | `Open Browser` | https://github.com | ${BROWSER} | alias=BrowserB | # BrowserB with first windows is opened. | + | ${location} | `Get Location` | | | # ${location} is: https://www.github.com | + | `Switch Window` | ${handle} | browser=BrowserA | | # BrowserA second windows is selected. | + | ${location} | `Get Location` | | | # ${location} = https://robocon.io/ | + | @{locations 1} | `Get Locations` | | | # By default, lists locations under the currectly active browser (BrowserA). | + | @{locations 2} | `Get Locations` | browser=ALL | | # By using browser=ALL argument keyword list all locations from all browsers. | + + The above example, @{locations 1} contains the following items: + https://robotframework.org/, https://robocon.io/ and + https://github.com/robotframework/'. The @{locations 2} + contains the following items: https://robotframework.org/, + https://robocon.io/, https://github.com/robotframework/' + and 'https://github.com/. + + = Browser and Driver options and service class = + + This section talks about how to configure either the browser or + the driver using the options and service arguments of the `Open + Browser` keyword. + + == Configuring the browser using the Selenium Options == + + As noted within the keyword documentation for `Open Browser`, its + ``options`` argument accepts Selenium options in two different + formats: as a string and as Python object which is an instance of + the Selenium options class. + + === Options string format === + + The string format allows defining Selenium options methods + or attributes and their arguments in Robot Framework test data. + The method and attributes names are case and space sensitive and + must match to the Selenium options methods and attributes names. + When defining a method, it must be defined in a similar way as in + python: method name, opening parenthesis, zero to many arguments + and closing parenthesis. If there is a need to define multiple + arguments for a single method, arguments must be separated with + comma, just like in Python. Example: `add_argument("--headless")` + or `add_experimental_option("key", "value")`. Attributes are + defined in a similar way as in Python: attribute name, equal sign, + and attribute value. Example, `headless=True`. Multiple methods + and attributes must be separated by a semicolon. Example: + `add_argument("--headless");add_argument("--start-maximized")`. + + Arguments allow defining Python data types and arguments are + evaluated by using Python + [https://docs.python.org/3/library/ast.html#ast.literal_eval|ast.literal_eval]. + Strings must be quoted with single or double quotes, example "value" + or 'value'. It is also possible to define other Python builtin + data types, example `True` or `None`, by not using quotes + around the arguments. + + The string format is space friendly. Usually, spaces do not alter + the defining methods or attributes. There are two exceptions. + In some Robot Framework test data formats, two or more spaces are + considered as cell separator and instead of defining a single + argument, two or more arguments may be defined. Spaces in string + arguments are not removed and are left as is. Example + `add_argument ( "--headless" )` is same as + `add_argument("--headless")`. But `add_argument(" --headless ")` is + not same same as `add_argument ( "--headless" )`, because + spaces inside of quotes are not removed. Please note that if + options string contains backslash, example a Windows OS path, + the backslash needs escaping both in Robot Framework data and + in Python side. This means single backslash must be writen using + four backslash characters. Example, Windows path: + "C:\\path\\to\\profile" must be written as + "C:\\\\\\\\path\\\\\\to\\\\\\\\profile". Another way to write + backslash is use Python + [https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals|raw strings] + and example write: r"C:\\\\path\\\\to\\\\profile". + + === Selenium Options as Python class === + + As last format, ``options`` argument also supports receiving + the Selenium options as Python class instance. In this case, the + instance is used as-is and the SeleniumLibrary will not convert + the instance to other formats. + For example, if the following code return value is saved to + `${options}` variable in the Robot Framework data: + | options = webdriver.ChromeOptions() + | options.add_argument('--disable-dev-shm-usage') + | return options + + Then the `${options}` variable can be used as an argument to + ``options``. + + Example the ``options`` argument can be used to launch Chomium-based + applications which utilize the + [https://bitbucket.org/chromiumembedded/cef/wiki/UsingChromeDriver|Chromium Embedded Framework] + . To launch Chromium-based application, use ``options`` to define + `binary_location` attribute and use `add_argument` method to define + `remote-debugging-port` port for the application. Once the browser + is opened, the test can interact with the embedded web-content of + the system under test. + + == Configuring the driver using the Service class == + + With the ``service`` argument, one can setup and configure the driver. For example + one can set the driver location and/port or specify the command line arguments. There + are several browser specific attributes related to logging as well. For the various + Service Class attributes refer to + [https://www.selenium.dev/documentation/webdriver/drivers/service/|the Selenium documentation] + . Currently the ``service`` argument only accepts Selenium service in the string format. + + === Service string format === + + The string format allows for defining Selenium service attributes + and their values in the `Open Browser` keyword. The attributes names + are case and space sensitive and must match to the Selenium attributes + names. Attributes are defined in a similar way as in Python: attribute + name, equal sign, and attribute value. Example, `port=1234`. Multiple + attributes must be separated by a semicolon. Example: + `executable_path='/path/to/driver';port=1234`. Don't have duplicate + attributes, like `service_args=['--append-log', '--readable-timestamp']; + service_args=['--log-level=DEBUG']` as the second will override the first. + Instead combine them as in + `service_args=['--append-log', '--readable-timestamp', '--log-level=DEBUG']` + + Arguments allow defining Python data types and arguments are + evaluated by using Python. Strings must be quoted with single + or double quotes, example "value" or 'value' + + = Timeouts, waits, and delays = + + This section discusses different ways how to wait for elements to + appear on web pages and to slow down execution speed otherwise. + It also explains the `time format` that can be used when setting various + timeouts, waits, and delays. + + == Timeout == + + SeleniumLibrary contains various keywords that have an optional + ``timeout`` argument that specifies how long these keywords should + wait for certain events or actions. These keywords include, for example, + ``Wait ...`` keywords and keywords related to alerts. Additionally + `Execute Async Javascript`. Although it does not have ``timeout``, + argument, uses a timeout to define how long asynchronous JavaScript + can run. + + The default timeout these keywords use can be set globally either by + using the `Set Selenium Timeout` keyword or with the ``timeout`` argument + when `importing` the library. If no default timeout is set globally, the + default is 5 seconds. If None is specified for the timeout argument in the + keywords, the default is used. See `time format` below for supported + timeout syntax. + + == Implicit wait == + + Implicit wait specifies the maximum time how long Selenium waits when + searching for elements. It can be set by using the `Set Selenium Implicit + Wait` keyword or with the ``implicit_wait`` argument when `importing` + the library. See [https://www.seleniumhq.org/docs/04_webdriver_advanced.jsp| + Selenium documentation] for more information about this functionality. + + See `time format` below for supported syntax. + + == Page load == + Page load timeout is the amount of time to wait for page load to complete + until a timeout exception is raised. + + The default page load timeout can be set globally + when `importing` the library with the ``page_load_timeout`` argument + or by using the `Set Selenium Page Load Timeout` keyword. + + See `time format` below for supported timeout syntax. + + Support for page load is new in SeleniumLibrary 6.1 + + == Selenium speed == + + Selenium execution speed can be slowed down globally by using `Set + Selenium speed` keyword. This functionality is designed to be used for + demonstrating or debugging purposes. Using it to make sure that elements + appear on a page is not a good idea. The above-explained timeouts + and waits should be used instead. + + See `time format` below for supported syntax. + + == Time format == + + All timeouts and waits can be given as numbers considered seconds + (e.g. ``0.5`` or ``42``) or in Robot Framework's time syntax + (e.g. ``1.5 seconds`` or ``1 min 30 s``). For more information about + the time syntax see the + [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#time-format|Robot Framework User Guide]. + + = Run-on-failure functionality = + + SeleniumLibrary has a handy feature that it can automatically execute + a keyword if any of its own keywords fails. By default, it uses the + `Capture Page Screenshot` keyword, but this can be changed either by + using the `Register Keyword To Run On Failure` keyword or with the + ``run_on_failure`` argument when `importing` the library. It is + possible to use any keyword from any imported library or resource file. + + The run-on-failure functionality can be disabled by using a special value + ``NOTHING`` or anything considered false (see `Boolean arguments`) + such as ``NONE``. + + = Boolean arguments = + + Starting from 5.0 SeleniumLibrary relies on Robot Framework to perform the + boolean conversion based on keyword arguments [https://docs.python.org/3/library/typing.html|type hint]. + More details in Robot Framework + [http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#supported-conversions|user guide] + + Please note SeleniumLibrary 3 and 4 did have own custom methods to covert + arguments to boolean values. + + = EventFiringWebDriver = + + 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.rst#EventFiringWebDriver|EventFiringWebDriver support] + documentation for further details. + + EventFiringWebDriver is new in SeleniumLibrary 4.0 + + = Thread support = + + SeleniumLibrary is not thread-safe. This is mainly due because the underlying + [https://github.com/SeleniumHQ/selenium/wiki/Frequently-Asked-Questions#q-is-webdriver-thread-safe| + Selenium tool is not thread-safe] within one browser/driver instance. + Because of the limitation in the Selenium side, the keywords or the + API provided by the SeleniumLibrary is not thread-safe. + + = Plugins = + + SeleniumLibrary offers plugins as a way to modify and add library keywords and modify some of the internal + functionality without creating a new library or hacking the source code. See + [https://github.com/robotframework/SeleniumLibrary/blob/master/docs/extending/extending.rst#Plugins|plugin API] + documentation for further details. + + Plugin API is new SeleniumLibrary 4.0 + + = Language = + + SeleniumLibrary offers the possibility to translate keyword names and documentation to new language. If language + is defined, SeleniumLibrary will search from + [https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#module-search-path | module search path] + for Python packages starting with `robotframework-seleniumlibrary-translation` by using the + [https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/ | Python pluging API]. The Library + is using naming convention to find Python plugins. + + The package must implement a single API call, ``get_language`` without any arguments. The method must return a + dictionary containing two keys: ``language`` and ``path``. The language key value defines which language + the package contains. Also the value should match (case insensitive) the library ``language`` import parameter. + The path parameter value should be full path to the translation file. + + == Translation file == + + The file name or extension is not important, but data must be in [https://www.json.org/json-en.html | json] + format. The keys of json are the methods names, not the keyword names, which implements keywords. Value of + key is json object which contains two keys: ``name`` and ``doc``. The ``name`` key contains the keyword + translated name and `doc` contains translated documentation. Providing doc and name are optional, example + translation json file can only provide translations to keyword names or only to documentation. But it is + always recommended to provide translation to both name and doc. Special key ``__intro__`` is for class level + documentation and ``__init__`` is for init level documentation. These special values ``name`` can not be + translated, instead ``name`` should be kept the same. + + == Generating template translation file == + + Template translation file, with English language can be created by running: + `rfselib translation /path/to/translation.json` command. Command does not provide translations to other + languages, it only provides easy way to create full list keywords and their documentation in correct + format. It is also possible to add keywords from library plugins by providing `--plugins` arguments + to command. Example: `rfselib translation --plugins myplugin.SomePlugin /path/to/translation.json` The + generated json file contains `sha256` key, which contains the sha256 sum of the library documentation. + The sha256 sum is used by `rfselib translation --compare /path/to/translation.json` command, which compares + the translation to the library and prints outs a table which tells if there are changes needed for + the translation file. + + Example project for translation can be found from + [https://github.com/MarketSquare/robotframework-seleniumlibrary-translation-fi | robotframework-seleniumlibrary-translation-fi] + repository. + """ + + ROBOT_LIBRARY_SCOPE = "GLOBAL" + ROBOT_LIBRARY_VERSION = __version__ + + def __init__( + self, + timeout=timedelta(seconds=5), + implicit_wait=timedelta(seconds=0), + run_on_failure="Capture Page Screenshot", + screenshot_root_directory: Optional[str] = None, + plugins: Optional[str] = None, + event_firing_webdriver: Optional[str] = None, + page_load_timeout=timedelta(minutes=5), + action_chain_delay=timedelta(seconds=0.25), + language: Optional[str] = None, + ): + """SeleniumLibrary can be imported with several optional arguments. + + - ``timeout``: + Default value for `timeouts` used with ``Wait ...`` keywords. + - ``implicit_wait``: + Default value for `implicit wait` used when locating elements. + - ``run_on_failure``: + Default action for the `run-on-failure functionality`. + - ``screenshot_root_directory``: + Path to folder where possible screenshots are created or EMBED. + See `Set Screenshot Directory` keyword for further details about EMBED. + If not given, the directory where the log file is written is used. + - ``plugins``: + Allows extending the SeleniumLibrary with external Python classes. + - ``event_firing_webdriver``: + Class for wrapping Selenium with + [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] + - ``page_load_timeout``: + Default value to wait for page load to complete until a timeout exception is raised. + - ``action_chain_delay``: + Default value for `ActionChains` delay to wait in between actions. + - ``language``: + Defines language which is used to translate keyword names and documentation. + """ + self.timeout = _convert_timeout(timeout) + self.implicit_wait = _convert_timeout(implicit_wait) + self.action_chain_delay = _convert_delay(action_chain_delay) + self.page_load_timeout = _convert_timeout(page_load_timeout) + self.speed = 0.0 + self.run_on_failure_keyword = RunOnFailureKeywords.resolve_keyword( + run_on_failure + ) + self._running_on_failure_keyword = False + self.screenshot_root_directory = screenshot_root_directory + self._resolve_screenshot_root_directory() + self._element_finder = ElementFinder(self) + self._plugin_keywords = [] + libraries = [ + AlertKeywords(self), + BrowserManagementKeywords(self), + CookieKeywords(self), + ElementKeywords(self), + ExpectedConditionKeywords(self), + FormElementKeywords(self), + FrameKeywords(self), + JavaScriptKeywords(self), + RunOnFailureKeywords(self), + ScreenshotKeywords(self), + SelectElementKeywords(self), + TableElementKeywords(self), + WaitingKeywords(self), + WindowKeywords(self), + ] + self.ROBOT_LIBRARY_LISTENER = LibraryListener() + self._running_keyword = None + self.event_firing_webdriver = None + if is_truthy(event_firing_webdriver): + self.event_firing_webdriver = self._parse_listener(event_firing_webdriver) + self._plugins = [] + if is_truthy(plugins): + plugin_libs = self._parse_plugins(plugins) + self._plugins = plugin_libs + libraries = libraries + plugin_libs + self._drivers = WebDriverCache() + translation_file = self._get_translation(language) + DynamicCore.__init__(self, libraries, translation_file) + + def run_keyword(self, name: str, args: tuple, kwargs: dict): + try: + return DynamicCore.run_keyword(self, name, args, kwargs) + except Exception: + self.failure_occurred() + raise + + def get_keyword_tags(self, name: str) -> list: + tags = list(DynamicCore.get_keyword_tags(self, name)) + if name in self._plugin_keywords: + tags.append("plugin") + return tags + + def get_keyword_documentation(self, name: str) -> str: + if name == "__intro__": + return self._get_intro_documentation() + return DynamicCore.get_keyword_documentation(self, name) + + def _parse_plugin_doc(self): + Doc = namedtuple("Doc", "doc, name") + for plugin in self._plugins: + yield Doc( + doc=getdoc(plugin) or "No plugin documentation found.", + name=plugin.__class__.__name__, + ) + + def _get_intro_documentation(self): + intro = DynamicCore.get_keyword_documentation(self, "__intro__") + for plugin_doc in self._parse_plugin_doc(): + intro = f"{intro}\n\n" + intro = f"{intro}= Plugin: {plugin_doc.name} =\n\n" + intro = f"{intro}{plugin_doc.doc}" + return intro + + def register_driver(self, driver: WebDriver, alias: str): + """Add's a `driver` to the library WebDriverCache. + + :param driver: Instance of the Selenium `WebDriver`. + :type driver: selenium.webdriver.remote.webdriver.WebDriver + :param alias: Alias given for this `WebDriver` instance. + :type alias: str + :return: The index of the `WebDriver` instance. + :rtype: int + """ + return self._drivers.register(driver, alias) + + def failure_occurred(self): + """Method that is executed when a SeleniumLibrary keyword fails. + + By default, executes the registered run-on-failure keyword. + Libraries extending SeleniumLibrary can overwrite this hook + method if they want to provide custom functionality instead. + """ + if self._running_on_failure_keyword or not self.run_on_failure_keyword: + return + try: + self._running_on_failure_keyword = True + if self.run_on_failure_keyword.lower() == "capture page screenshot": + self.capture_page_screenshot() + else: + BuiltIn().run_keyword(self.run_on_failure_keyword) + except Exception as err: + logger.warn( + f"Keyword '{self.run_on_failure_keyword}' could not be run on failure: {err}" + ) + finally: + self._running_on_failure_keyword = False + + @property + def driver(self) -> WebDriver: + """Current active driver. + + :rtype: selenium.webdriver.remote.webdriver.WebDriver + :raises SeleniumLibrary.errors.NoOpenBrowser: If browser is not open. + """ + if not self._drivers.current: + raise NoOpenBrowser("No browser is open.") + return self._drivers.current + + def find_element( + self, locator: str, parent: Optional[WebElement] = None + ) -> WebElement: + """Find element matching `locator`. + + :param locator: Locator to use when searching the element. + See library documentation for the supported locator syntax. + :type locator: str or selenium.webdriver.remote.webelement.WebElement + :param parent: Optional parent `WebElememt` to search child elements + from. By default, search starts from the root using `WebDriver`. + :type parent: selenium.webdriver.remote.webelement.WebElement + :return: Found `WebElement`. + :rtype: selenium.webdriver.remote.webelement.WebElement + :raises SeleniumLibrary.errors.ElementNotFound: If element not found. + """ + return self._element_finder.find(locator, parent=parent) + + def find_elements( + self, locator: str, parent: WebElement = None + ) -> List[WebElement]: + """Find all elements matching `locator`. + + :param locator: Locator to use when searching the element. + See library documentation for the supported locator syntax. + :type locator: str or selenium.webdriver.remote.webelement.WebElement + :param parent: Optional parent `WebElememt` to search child elements + from. By default, search starts from the root using `WebDriver`. + :type parent: selenium.webdriver.remote.webelement.WebElement + :return: list of found `WebElement` or e,mpty if elements are not found. + :rtype: list[selenium.webdriver.remote.webelement.WebElement] + """ + return self._element_finder.find( + locator, first_only=False, required=False, parent=parent + ) + + def _parse_plugins(self, plugins): + libraries = [] + importer = Importer("test library") + for parsed_plugin in self._string_to_modules(plugins): + plugin = importer.import_class_or_module(parsed_plugin.module) + if not isclass(plugin): + message = f"Importing test library: '{parsed_plugin.module}' failed." + raise DataError(message) + plugin = plugin(self, *parsed_plugin.args, **parsed_plugin.kw_args) + if not isinstance(plugin, LibraryComponent): + message = ( + "Plugin does not inherit SeleniumLibrary.base.LibraryComponent" + ) + raise PluginError(message) + self._store_plugin_keywords(plugin) + libraries.append(plugin) + return libraries + + def _parse_listener(self, event_firing_webdriver): + listener_module = self._string_to_modules(event_firing_webdriver) + listener_count = len(listener_module) + if listener_count > 1: + message = f"Is is possible import only one listener but there was {listener_count} listeners." + raise ValueError(message) + listener_module = listener_module[0] + importer = Importer("test library") + listener = importer.import_class_or_module(listener_module.module) + if not isclass(listener): + message = f"Importing test Selenium lister class '{listener_module.module}' failed." + raise DataError(message) + return listener + + def _string_to_modules(self, modules): + Module = namedtuple("Module", "module, args, kw_args") + parsed_modules = [] + for module in modules.split(","): + module = module.strip() + module_and_args = module.split(";") + module_name = module_and_args.pop(0) + kw_args = {} + args = [] + for argument in module_and_args: + if "=" in argument: + key, value = argument.split("=") + kw_args[key] = value + else: + args.append(argument) + module = Module(module=module_name, args=args, kw_args=kw_args) + parsed_modules.append(module) + return parsed_modules + + def _store_plugin_keywords(self, plugin): + dynamic_core = DynamicCore([plugin]) + self._plugin_keywords.extend(dynamic_core.get_keyword_names()) + + def _resolve_screenshot_root_directory(self): + screenshot_root_directory = self.screenshot_root_directory + if is_string(screenshot_root_directory): + if screenshot_root_directory.upper() == EMBED: + self.screenshot_root_directory = EMBED + + @staticmethod + def _get_translation(language: Union[str, None]) -> Union[Path, None]: + if not language: + return None + discovered_plugins = { + name: importlib.import_module(name) + for _, name, _ in pkgutil.iter_modules() + if name.startswith("robotframework_seleniumlibrary_translation") + } + for plugin in discovered_plugins.values(): + try: + data = plugin.get_language() + except AttributeError: + continue + if data.get("language", "").lower() == language.lower() and data.get( + "path" + ): + return Path(data.get("path")).absolute() + return None From f9cd72ab81717ae1d508a5849bbd9e19eb94937d Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 16:14:38 -0400 Subject: [PATCH 074/122] Generate stub file for 6.5.0 --- src/SeleniumLibrary/__init__.pyi | 426 +++++++++++++++---------------- 1 file changed, 213 insertions(+), 213 deletions(-) diff --git a/src/SeleniumLibrary/__init__.pyi b/src/SeleniumLibrary/__init__.pyi index 2d8f09c12..d17efdd1c 100644 --- a/src/SeleniumLibrary/__init__.pyi +++ b/src/SeleniumLibrary/__init__.pyi @@ -1,213 +1,213 @@ -from datetime import timedelta -from typing import Any, Optional, Union - -import selenium -from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.remote.webelement import WebElement - -class SeleniumLibrary: - def __init__(self, timeout = timedelta(seconds=5.0), implicit_wait = timedelta(seconds=0.0), run_on_failure = 'Capture Page Screenshot', screenshot_root_directory: Optional[Optional] = None, plugins: Optional[Optional] = None, event_firing_webdriver: Optional[Optional] = None, page_load_timeout = timedelta(seconds=300.0), action_chain_delay = timedelta(seconds=0.25), language: Optional[Optional] = None): ... - def add_cookie(self, name: str, value: str, path: Optional[Optional] = None, domain: Optional[Optional] = None, secure: Optional[Optional] = None, expiry: Optional[Optional] = None): ... - def add_location_strategy(self, strategy_name: str, strategy_keyword: str, persist: bool = False): ... - def alert_should_be_present(self, text: str = '', action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... - def alert_should_not_be_present(self, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... - def assign_id_to_element(self, locator: Union, id: str): ... - def capture_element_screenshot(self, locator: Union, filename: str = 'selenium-element-screenshot-{index}.png'): ... - def capture_page_screenshot(self, filename: str = 'selenium-screenshot-{index}.png'): ... - def checkbox_should_be_selected(self, locator: Union): ... - def checkbox_should_not_be_selected(self, locator: Union): ... - def choose_file(self, locator: Union, file_path: str): ... - def clear_element_text(self, locator: Union): ... - def click_button(self, locator: Union, modifier: Union = False): ... - def click_element(self, locator: Union, modifier: Union = False, action_chain: bool = False): ... - def click_element_at_coordinates(self, locator: Union, xoffset: int, yoffset: int): ... - def click_image(self, locator: Union, modifier: Union = False): ... - def click_link(self, locator: Union, modifier: Union = False): ... - def close_all_browsers(self): ... - def close_browser(self): ... - def close_window(self): ... - def cover_element(self, locator: Union): ... - def create_webdriver(self, driver_name: str, alias: Optional[Optional] = None, kwargs: Optional[Optional] = None, **init_kwargs): ... - def current_frame_should_contain(self, text: str, loglevel: str = 'TRACE'): ... - def current_frame_should_not_contain(self, text: str, loglevel: str = 'TRACE'): ... - def delete_all_cookies(self): ... - def delete_cookie(self, name): ... - def double_click_element(self, locator: Union): ... - def drag_and_drop(self, locator: Union, target: Union): ... - def drag_and_drop_by_offset(self, locator: Union, xoffset: int, yoffset: int): ... - def element_attribute_value_should_be(self, locator: Union, attribute: str, expected: Optional, message: Optional[Optional] = None): ... - def element_should_be_disabled(self, locator: Union): ... - def element_should_be_enabled(self, locator: Union): ... - def element_should_be_focused(self, locator: Union): ... - def element_should_be_visible(self, locator: Union, message: Optional[Optional] = None): ... - def element_should_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... - def element_should_not_be_visible(self, locator: Union, message: Optional[Optional] = None): ... - def element_should_not_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... - def element_text_should_be(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... - def element_text_should_not_be(self, locator: Union, not_expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... - def execute_async_javascript(self, *code: Any): ... - def execute_javascript(self, *code: Any): ... - def frame_should_contain(self, locator: Union, text: str, loglevel: str = 'TRACE'): ... - def get_action_chain_delay(self): ... - def get_all_links(self): ... - def get_browser_aliases(self): ... - def get_browser_ids(self): ... - def get_cookie(self, name: str): ... - def get_cookies(self, as_dict: bool = False): ... - def get_dom_attribute(self, locator: Union, attribute: str): ... - def get_element_attribute(self, locator: Union, attribute: str): ... - def get_element_count(self, locator: Union): ... - def get_element_size(self, locator: Union): ... - def get_horizontal_position(self, locator: Union): ... - def get_list_items(self, locator: Union, values: bool = False): ... - def get_location(self): ... - def get_locations(self, browser: str = 'CURRENT'): ... - def get_property(self, locator: Union, property: str): ... - def get_selected_list_label(self, locator: Union): ... - def get_selected_list_labels(self, locator: Union): ... - def get_selected_list_value(self, locator: Union): ... - def get_selected_list_values(self, locator: Union): ... - def get_selenium_implicit_wait(self): ... - def get_selenium_page_load_timeout(self): ... - def get_selenium_speed(self): ... - def get_selenium_timeout(self): ... - def get_session_id(self): ... - def get_source(self): ... - def get_table_cell(self, locator: Union, row: int, column: int, loglevel: str = 'TRACE'): ... - def get_text(self, locator: Union): ... - def get_title(self): ... - def get_value(self, locator: Union): ... - def get_vertical_position(self, locator: Union): ... - def get_webelement(self, locator: Union): ... - def get_webelements(self, locator: Union): ... - def get_window_handles(self, browser: str = 'CURRENT'): ... - def get_window_identifiers(self, browser: str = 'CURRENT'): ... - def get_window_names(self, browser: str = 'CURRENT'): ... - def get_window_position(self): ... - def get_window_size(self, inner: bool = False): ... - def get_window_titles(self, browser: str = 'CURRENT'): ... - def go_back(self): ... - def go_to(self, url): ... - def handle_alert(self, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... - def input_password(self, locator: Union, password: str, clear: bool = True): ... - def input_text(self, locator: Union, text: str, clear: bool = True): ... - def input_text_into_alert(self, text: str, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... - def list_selection_should_be(self, locator: Union, *expected: str): ... - def list_should_have_no_selections(self, locator: Union): ... - def location_should_be(self, url: str, message: Optional[Optional] = None): ... - def location_should_contain(self, expected: str, message: Optional[Optional] = None): ... - def log_location(self): ... - def log_source(self, loglevel: str = 'INFO'): ... - def log_title(self): ... - def maximize_browser_window(self): ... - def minimize_browser_window(self): ... - def mouse_down(self, locator: Union): ... - def mouse_down_on_image(self, locator: Union): ... - def mouse_down_on_link(self, locator: Union): ... - def mouse_out(self, locator: Union): ... - def mouse_over(self, locator: Union): ... - def mouse_up(self, locator: Union): ... - def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Any] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None, service: Optional[Any] = None): ... - def open_context_menu(self, locator: Union): ... - def page_should_contain(self, text: str, loglevel: str = 'TRACE'): ... - def page_should_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_checkbox(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_element(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE', limit: Optional[Optional] = None): ... - def page_should_contain_image(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_link(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_list(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_radio_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_contain_textfield(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain(self, text: str, loglevel: str = 'TRACE'): ... - def page_should_not_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_checkbox(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_element(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_image(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_link(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_list(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_radio_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def page_should_not_contain_textfield(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... - def press_key(self, locator: Union, key: str): ... - def press_keys(self, locator: Optional[Union] = None, *keys: str): ... - def print_page_as_pdf(self, filename: str = 'selenium-page-{index}.pdf', background: Optional[Optional] = None, margin_bottom: Optional[Optional] = None, margin_left: Optional[Optional] = None, margin_right: Optional[Optional] = None, margin_top: Optional[Optional] = None, orientation: Optional[Optional] = None, page_height: Optional[Optional] = None, page_ranges: Optional[Optional] = None, page_width: Optional[Optional] = None, scale: Optional[Optional] = None, shrink_to_fit: Optional[Optional] = None): ... - def radio_button_should_be_set_to(self, group_name: str, value: str): ... - def radio_button_should_not_be_selected(self, group_name: str): ... - def register_keyword_to_run_on_failure(self, keyword: Optional): ... - def reload_page(self): ... - def remove_location_strategy(self, strategy_name: str): ... - def scroll_element_into_view(self, locator: Union): ... - def select_all_from_list(self, locator: Union): ... - def select_checkbox(self, locator: Union): ... - def select_frame(self, locator: Union): ... - def select_from_list_by_index(self, locator: Union, *indexes: str): ... - def select_from_list_by_label(self, locator: Union, *labels: str): ... - def select_from_list_by_value(self, locator: Union, *values: str): ... - def select_radio_button(self, group_name: str, value: str): ... - def set_action_chain_delay(self, value: timedelta): ... - def set_browser_implicit_wait(self, value: timedelta): ... - def set_focus_to_element(self, locator: Union): ... - def set_screenshot_directory(self, path: Optional): ... - def set_selenium_implicit_wait(self, value: timedelta): ... - def set_selenium_page_load_timeout(self, value: timedelta): ... - def set_selenium_speed(self, value: timedelta): ... - def set_selenium_timeout(self, value: timedelta): ... - def set_window_position(self, x: int, y: int): ... - def set_window_size(self, width: int, height: int, inner: bool = False): ... - def simulate_event(self, locator: Union, event: str): ... - def submit_form(self, locator: Optional[Union] = None): ... - def switch_browser(self, index_or_alias: str): ... - def switch_window(self, locator: Union = MAIN, timeout: Optional[Optional] = None, browser: str = 'CURRENT'): ... - def table_cell_should_contain(self, locator: Union, row: int, column: int, expected: str, loglevel: str = 'TRACE'): ... - def table_column_should_contain(self, locator: Union, column: int, expected: str, loglevel: str = 'TRACE'): ... - def table_footer_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... - def table_header_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... - def table_row_should_contain(self, locator: Union, row: int, expected: str, loglevel: str = 'TRACE'): ... - def table_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... - def textarea_should_contain(self, locator: Union, expected: str, message: Optional[Optional] = None): ... - def textarea_value_should_be(self, locator: Union, expected: str, message: Optional[Optional] = None): ... - def textfield_should_contain(self, locator: Union, expected: str, message: Optional[Optional] = None): ... - def textfield_value_should_be(self, locator: Union, expected: str, message: Optional[Optional] = None): ... - def title_should_be(self, title: str, message: Optional[Optional] = None): ... - def unselect_all_from_list(self, locator: Union): ... - def unselect_checkbox(self, locator: Union): ... - def unselect_frame(self): ... - def unselect_from_list_by_index(self, locator: Union, *indexes: str): ... - def unselect_from_list_by_label(self, locator: Union, *labels: str): ... - def unselect_from_list_by_value(self, locator: Union, *values: str): ... - def wait_for_condition(self, condition: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_for_expected_condition(self, condition: string, *args, timeout: Optional = 10): ... - def wait_until_element_contains(self, locator: Union, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_element_does_not_contain(self, locator: Union, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_element_is_enabled(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_element_is_not_visible(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_element_is_visible(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_location_contains(self, expected: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... - def wait_until_location_does_not_contain(self, location: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... - def wait_until_location_is(self, expected: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... - def wait_until_location_is_not(self, location: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... - def wait_until_page_contains(self, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_page_contains_element(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None, limit: Optional[Optional] = None): ... - def wait_until_page_does_not_contain(self, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... - def wait_until_page_does_not_contain_element(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None, limit: Optional[Optional] = None): ... - # methods from library. - def add_library_components(self, library_components): ... - def get_keyword_names(self): ... - def run_keyword(self, name: str, args: tuple, kwargs: Optional[dict] = None): ... - def get_keyword_arguments(self, name: str): ... - def get_keyword_tags(self, name: str): ... - def get_keyword_documentation(self, name: str): ... - def get_keyword_types(self, name: str): ... - def get_keyword_source(self, keyword_name: str): ... - def failure_occurred(self): ... - def register_driver(self, driver: WebDriver, alias: str): ... - @property - def driver(self) -> WebDriver: ... - def find_element(self, locator: str, parent: Optional[WebElement] = None): ... - def find_elements(self, locator: str, parent: WebElement = None): ... - def _parse_plugins(self, plugins: Any): ... - def _parse_plugin_doc(self): ... - def _get_intro_documentation(self): ... - def _parse_listener(self, event_firing_webdriver: Any): ... - def _string_to_modules(self, modules: Any): ... - def _store_plugin_keywords(self, plugin): ... - def _resolve_screenshot_root_directory(self): ... +from datetime import timedelta +from typing import Any, Optional, Union + +import selenium +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.remote.webelement import WebElement + +class SeleniumLibrary: + def __init__(self, timeout = timedelta(seconds=5.0), implicit_wait = timedelta(seconds=0.0), run_on_failure = 'Capture Page Screenshot', screenshot_root_directory: Optional[Optional] = None, plugins: Optional[Optional] = None, event_firing_webdriver: Optional[Optional] = None, page_load_timeout = timedelta(seconds=300.0), action_chain_delay = timedelta(seconds=0.25), language: Optional[Optional] = None): ... + def add_cookie(self, name: str, value: str, path: Optional[Optional] = None, domain: Optional[Optional] = None, secure: Optional[Optional] = None, expiry: Optional[Optional] = None): ... + def add_location_strategy(self, strategy_name: str, strategy_keyword: str, persist: bool = False): ... + def alert_should_be_present(self, text: str = '', action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... + def alert_should_not_be_present(self, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... + def assign_id_to_element(self, locator: Union, id: str): ... + def capture_element_screenshot(self, locator: Union, filename: str = 'selenium-element-screenshot-{index}.png'): ... + def capture_page_screenshot(self, filename: str = 'selenium-screenshot-{index}.png'): ... + def checkbox_should_be_selected(self, locator: Union): ... + def checkbox_should_not_be_selected(self, locator: Union): ... + def choose_file(self, locator: Union, file_path: str): ... + def clear_element_text(self, locator: Union): ... + def click_button(self, locator: Union, modifier: Union = False): ... + def click_element(self, locator: Union, modifier: Union = False, action_chain: bool = False): ... + def click_element_at_coordinates(self, locator: Union, xoffset: int, yoffset: int): ... + def click_image(self, locator: Union, modifier: Union = False): ... + def click_link(self, locator: Union, modifier: Union = False): ... + def close_all_browsers(self): ... + def close_browser(self): ... + def close_window(self): ... + def cover_element(self, locator: Union): ... + def create_webdriver(self, driver_name: str, alias: Optional[Optional] = None, kwargs: Optional[Optional] = None, **init_kwargs): ... + def current_frame_should_contain(self, text: str, loglevel: str = 'TRACE'): ... + def current_frame_should_not_contain(self, text: str, loglevel: str = 'TRACE'): ... + def delete_all_cookies(self): ... + def delete_cookie(self, name): ... + def double_click_element(self, locator: Union): ... + def drag_and_drop(self, locator: Union, target: Union): ... + def drag_and_drop_by_offset(self, locator: Union, xoffset: int, yoffset: int): ... + def element_attribute_value_should_be(self, locator: Union, attribute: str, expected: Optional, message: Optional[Optional] = None): ... + def element_should_be_disabled(self, locator: Union): ... + def element_should_be_enabled(self, locator: Union): ... + def element_should_be_focused(self, locator: Union): ... + def element_should_be_visible(self, locator: Union, message: Optional[Optional] = None): ... + def element_should_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... + def element_should_not_be_visible(self, locator: Union, message: Optional[Optional] = None): ... + def element_should_not_contain(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... + def element_text_should_be(self, locator: Union, expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... + def element_text_should_not_be(self, locator: Union, not_expected: Optional, message: Optional[Optional] = None, ignore_case: bool = False): ... + def execute_async_javascript(self, *code: Any): ... + def execute_javascript(self, *code: Any): ... + def frame_should_contain(self, locator: Union, text: str, loglevel: str = 'TRACE'): ... + def get_action_chain_delay(self): ... + def get_all_links(self): ... + def get_browser_aliases(self): ... + def get_browser_ids(self): ... + def get_cookie(self, name: str): ... + def get_cookies(self, as_dict: bool = False): ... + def get_dom_attribute(self, locator: Union, attribute: str): ... + def get_element_attribute(self, locator: Union, attribute: str): ... + def get_element_count(self, locator: Union): ... + def get_element_size(self, locator: Union): ... + def get_horizontal_position(self, locator: Union): ... + def get_list_items(self, locator: Union, values: bool = False): ... + def get_location(self): ... + def get_locations(self, browser: str = 'CURRENT'): ... + def get_property(self, locator: Union, property: str): ... + def get_selected_list_label(self, locator: Union): ... + def get_selected_list_labels(self, locator: Union): ... + def get_selected_list_value(self, locator: Union): ... + def get_selected_list_values(self, locator: Union): ... + def get_selenium_implicit_wait(self): ... + def get_selenium_page_load_timeout(self): ... + def get_selenium_speed(self): ... + def get_selenium_timeout(self): ... + def get_session_id(self): ... + def get_source(self): ... + def get_table_cell(self, locator: Union, row: int, column: int, loglevel: str = 'TRACE'): ... + def get_text(self, locator: Union): ... + def get_title(self): ... + def get_value(self, locator: Union): ... + def get_vertical_position(self, locator: Union): ... + def get_webelement(self, locator: Union): ... + def get_webelements(self, locator: Union): ... + def get_window_handles(self, browser: str = 'CURRENT'): ... + def get_window_identifiers(self, browser: str = 'CURRENT'): ... + def get_window_names(self, browser: str = 'CURRENT'): ... + def get_window_position(self): ... + def get_window_size(self, inner: bool = False): ... + def get_window_titles(self, browser: str = 'CURRENT'): ... + def go_back(self): ... + def go_to(self, url): ... + def handle_alert(self, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... + def input_password(self, locator: Union, password: str, clear: bool = True): ... + def input_text(self, locator: Union, text: str, clear: bool = True): ... + def input_text_into_alert(self, text: str, action: str = 'ACCEPT', timeout: Optional[Optional] = None): ... + def list_selection_should_be(self, locator: Union, *expected: str): ... + def list_should_have_no_selections(self, locator: Union): ... + def location_should_be(self, url: str, message: Optional[Optional] = None): ... + def location_should_contain(self, expected: str, message: Optional[Optional] = None): ... + def log_location(self): ... + def log_source(self, loglevel: str = 'INFO'): ... + def log_title(self): ... + def maximize_browser_window(self): ... + def minimize_browser_window(self): ... + def mouse_down(self, locator: Union): ... + def mouse_down_on_image(self, locator: Union): ... + def mouse_down_on_link(self, locator: Union): ... + def mouse_out(self, locator: Union): ... + def mouse_over(self, locator: Union): ... + def mouse_up(self, locator: Union): ... + def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Optional] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None, service: Optional[Optional] = None): ... + def open_context_menu(self, locator: Union): ... + def page_should_contain(self, text: str, loglevel: str = 'TRACE'): ... + def page_should_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_checkbox(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_element(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE', limit: Optional[Optional] = None): ... + def page_should_contain_image(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_link(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_list(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_radio_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_contain_textfield(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain(self, text: str, loglevel: str = 'TRACE'): ... + def page_should_not_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_checkbox(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_element(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_image(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_link(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_list(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_radio_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def page_should_not_contain_textfield(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... + def press_key(self, locator: Union, key: str): ... + def press_keys(self, locator: Optional[Union] = None, *keys: str): ... + def print_page_as_pdf(self, filename: str = 'selenium-page-{index}.pdf', background: Optional[Optional] = None, margin_bottom: Optional[Optional] = None, margin_left: Optional[Optional] = None, margin_right: Optional[Optional] = None, margin_top: Optional[Optional] = None, orientation: Optional[Optional] = None, page_height: Optional[Optional] = None, page_ranges: Optional[Optional] = None, page_width: Optional[Optional] = None, scale: Optional[Optional] = None, shrink_to_fit: Optional[Optional] = None): ... + def radio_button_should_be_set_to(self, group_name: str, value: str): ... + def radio_button_should_not_be_selected(self, group_name: str): ... + def register_keyword_to_run_on_failure(self, keyword: Optional): ... + def reload_page(self): ... + def remove_location_strategy(self, strategy_name: str): ... + def scroll_element_into_view(self, locator: Union): ... + def select_all_from_list(self, locator: Union): ... + def select_checkbox(self, locator: Union): ... + def select_frame(self, locator: Union): ... + def select_from_list_by_index(self, locator: Union, *indexes: str): ... + def select_from_list_by_label(self, locator: Union, *labels: str): ... + def select_from_list_by_value(self, locator: Union, *values: str): ... + def select_radio_button(self, group_name: str, value: str): ... + def set_action_chain_delay(self, value: timedelta): ... + def set_browser_implicit_wait(self, value: timedelta): ... + def set_focus_to_element(self, locator: Union): ... + def set_screenshot_directory(self, path: Optional): ... + def set_selenium_implicit_wait(self, value: timedelta): ... + def set_selenium_page_load_timeout(self, value: timedelta): ... + def set_selenium_speed(self, value: timedelta): ... + def set_selenium_timeout(self, value: timedelta): ... + def set_window_position(self, x: int, y: int): ... + def set_window_size(self, width: int, height: int, inner: bool = False): ... + def simulate_event(self, locator: Union, event: str): ... + def submit_form(self, locator: Optional[Union] = None): ... + def switch_browser(self, index_or_alias: str): ... + def switch_window(self, locator: Union = MAIN, timeout: Optional[Optional] = None, browser: str = 'CURRENT'): ... + def table_cell_should_contain(self, locator: Union, row: int, column: int, expected: str, loglevel: str = 'TRACE'): ... + def table_column_should_contain(self, locator: Union, column: int, expected: str, loglevel: str = 'TRACE'): ... + def table_footer_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... + def table_header_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... + def table_row_should_contain(self, locator: Union, row: int, expected: str, loglevel: str = 'TRACE'): ... + def table_should_contain(self, locator: Union, expected: str, loglevel: str = 'TRACE'): ... + def textarea_should_contain(self, locator: Union, expected: str, message: Optional[Optional] = None): ... + def textarea_value_should_be(self, locator: Union, expected: str, message: Optional[Optional] = None): ... + def textfield_should_contain(self, locator: Union, expected: str, message: Optional[Optional] = None): ... + def textfield_value_should_be(self, locator: Union, expected: str, message: Optional[Optional] = None): ... + def title_should_be(self, title: str, message: Optional[Optional] = None): ... + def unselect_all_from_list(self, locator: Union): ... + def unselect_checkbox(self, locator: Union): ... + def unselect_frame(self): ... + def unselect_from_list_by_index(self, locator: Union, *indexes: str): ... + def unselect_from_list_by_label(self, locator: Union, *labels: str): ... + def unselect_from_list_by_value(self, locator: Union, *values: str): ... + def wait_for_condition(self, condition: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_for_expected_condition(self, condition: string, *args, timeout: Optional = 10): ... + def wait_until_element_contains(self, locator: Union, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_element_does_not_contain(self, locator: Union, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_element_is_enabled(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_element_is_not_visible(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_element_is_visible(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_location_contains(self, expected: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... + def wait_until_location_does_not_contain(self, location: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... + def wait_until_location_is(self, expected: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... + def wait_until_location_is_not(self, location: str, timeout: Optional[Optional] = None, message: Optional[Optional] = None): ... + def wait_until_page_contains(self, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_page_contains_element(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None, limit: Optional[Optional] = None): ... + def wait_until_page_does_not_contain(self, text: str, timeout: Optional[Optional] = None, error: Optional[Optional] = None): ... + def wait_until_page_does_not_contain_element(self, locator: Union, timeout: Optional[Optional] = None, error: Optional[Optional] = None, limit: Optional[Optional] = None): ... + # methods from library. + def add_library_components(self, library_components): ... + def get_keyword_names(self): ... + def run_keyword(self, name: str, args: tuple, kwargs: Optional[dict] = None): ... + def get_keyword_arguments(self, name: str): ... + def get_keyword_tags(self, name: str): ... + def get_keyword_documentation(self, name: str): ... + def get_keyword_types(self, name: str): ... + def get_keyword_source(self, keyword_name: str): ... + def failure_occurred(self): ... + def register_driver(self, driver: WebDriver, alias: str): ... + @property + def driver(self) -> WebDriver: ... + def find_element(self, locator: str, parent: Optional[WebElement] = None): ... + def find_elements(self, locator: str, parent: WebElement = None): ... + def _parse_plugins(self, plugins: Any): ... + def _parse_plugin_doc(self): ... + def _get_intro_documentation(self): ... + def _parse_listener(self, event_firing_webdriver: Any): ... + def _string_to_modules(self, modules: Any): ... + def _store_plugin_keywords(self, plugin): ... + def _resolve_screenshot_root_directory(self): ... From 00ac2cd9a191c091c908749ff3ed091d70ed0279 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 16:15:09 -0400 Subject: [PATCH 075/122] Generated docs for version 6.5.0 --- docs/SeleniumLibrary.html | 3746 ++++++++++++++++++------------------- 1 file changed, 1873 insertions(+), 1873 deletions(-) diff --git a/docs/SeleniumLibrary.html b/docs/SeleniumLibrary.html index 49a9a757a..cf4ebe698 100644 --- a/docs/SeleniumLibrary.html +++ b/docs/SeleniumLibrary.html @@ -1,1873 +1,1873 @@ - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Opening library documentation failed

-
    -
  • Verify that you have JavaScript enabled in your browser.
  • -
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • -
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • -
-
- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Opening library documentation failed

+
    +
  • Verify that you have JavaScript enabled in your browser.
  • +
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • +
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
+
+ + + + + + + + + + + + + + + + From f74ea79449ca7b1de97e4b3c1143134fdc1035d4 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sat, 15 Jun 2024 16:16:00 -0400 Subject: [PATCH 076/122] Regenerated project docs --- docs/index.html | 368 ++++++++++++++++++++++++------------------------ 1 file changed, 184 insertions(+), 184 deletions(-) diff --git a/docs/index.html b/docs/index.html index 9ecf5168b..d6adb5082 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,184 +1,184 @@ - - - - - - -SeleniumLibrary - - - - -
-

SeleniumLibrary

- - -
-

Introduction

-

SeleniumLibrary is a web testing library for Robot Framework that -utilizes the Selenium tool internally. The project is hosted on GitHub -and downloads can be found from PyPI.

-

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.11. -In addition to the normal Python interpreter, it works also -with PyPy.

-

SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to -Selenium2Library and then later renamed back to SeleniumLibrary. -See the VERSIONS.rst for more information about different versions and the -overall project history.

- -https://img.shields.io/pypi/v/robotframework-seleniumlibrary.svg?label=version - - -https://img.shields.io/pypi/dm/robotframework-seleniumlibrary.svg - - -https://img.shields.io/pypi/l/robotframework-seleniumlibrary.svg - - -https://github.com/robotframework/SeleniumLibrary/actions/workflows/CI.yml/badge.svg?branch=master - -
-
-

Keyword Documentation

-

See keyword documentation for available keywords and more information -about the library in general.

-
-
-

Installation

-

The recommended installation method is using pip:

-
pip install --upgrade robotframework-seleniumlibrary
-

Running this command installs also the latest Selenium and Robot Framework -versions, but you still need to install browser drivers separately. -The --upgrade option can be omitted when installing the library for the -first time.

-

It is possible to install directly from the GitHub repository. To install -latest source from the master branch, use this command:

-
pip install git+https://github.com/robotframework/SeleniumLibrary.git
-

Please note that installation will take some time, because pip will -clone the SeleniumLibrary project to a temporary directory and then -perform the installation.

-

See Robot Framework installation instructions for detailed information -about installing Python and Robot Framework itself. For more details about -using pip see its own documentation.

-
-
-

Browser drivers

-

After installing the library, you still need to install browser and -operating system specific browser drivers for all those browsers you -want to use in tests. These are the exact same drivers you need to use with -Selenium also when not using SeleniumLibrary. More information about -drivers can be found from Selenium documentation.

-

The general approach to install a browser driver is downloading a right -driver, such as chromedriver for Chrome, and placing it into -a directory that is in PATH. Drivers for different browsers -can be found via Selenium documentation or by using your favorite -search engine with a search term like selenium chrome browser driver. -New browser driver versions are released to support features in -new browsers, fix bug, or otherwise, and you need to keep an eye on them -to know when to update drivers you use.

-

Alternatively, you can use a tool called WebdriverManager which can -find the latest version or when required, any version of appropriate -webdrivers for you and then download and link/copy it into right -location. Tool can run on all major operating systems and supports -downloading of Chrome, Firefox, Opera & Edge webdrivers.

-

Here's an example:

-
pip install webdrivermanager
-webdrivermanager firefox chrome --linkpath /usr/local/bin
-
-
-

Usage

-

To use SeleniumLibrary in Robot Framework tests, the library needs to -first be imported using the Library setting as any other library. -The library accepts some import time arguments, which are documented -in the keyword documentation along with all the keywords provided -by the library.

-

When using Robot Framework, it is generally recommended to write as -easy-to-understand tests as possible. The keywords provided by -SeleniumLibrary is pretty low level, though, and often require -implementation-specific arguments like element locators to be passed -as arguments. It is thus typically a good idea to write tests using -Robot Framework's higher-level keywords that utilize SeleniumLibrary -keywords internally. This is illustrated by the following example -where SeleniumLibrary keywords like Input Text are primarily -used by higher-level keywords like Input Username.

-
*** Settings ***
-Documentation     Simple example using SeleniumLibrary.
-Library           SeleniumLibrary
-
-*** Variables ***
-${LOGIN URL}      http://localhost:7272
-${BROWSER}        Chrome
-
-*** Test Cases ***
-Valid Login
-    Open Browser To Login Page
-    Input Username    demo
-    Input Password    mode
-    Submit Credentials
-    Welcome Page Should Be Open
-    [Teardown]    Close Browser
-
-*** Keywords ***
-Open Browser To Login Page
-    Open Browser    ${LOGIN URL}    ${BROWSER}
-    Title Should Be    Login Page
-
-Input Username
-    [Arguments]    ${username}
-    Input Text    username_field    ${username}
-
-Input Password
-    [Arguments]    ${password}
-    Input Text    password_field    ${password}
-
-Submit Credentials
-    Click Button    login_button
-
-Welcome Page Should Be Open
-    Title Should Be    Welcome Page
-

The above example is a slightly modified version of an example in a -demo project that illustrates using Robot Framework and SeleniumLibrary. -See the demo for more examples that you can also execute on your own -machine. For more information about Robot Framework test data syntax in -general see the Robot Framework User Guide.

-
-
-

Extending SeleniumLibrary

-

Before creating your own library which extends the SeleniumLibrary, please consider would -the extension be also useful also for general usage. If it could be useful also for general -usage, please create a new issue describing the enhancement request and even better if the -issue is backed up by a pull request.

-

If the enhancement is not generally useful, example solution is domain specific, then the -SeleniumLibrary offers public APIs which can be used to build its own plugins and libraries. -Plugin API allows us to add new keywords, modify existing keywords and modify the internal -functionality of the library. Also new libraries can be built on top of the -SeleniumLibrary. Please see extending documentation for more details about the -available methods and for examples how the library can be extended.

-
-
-

Community

-

If the provided documentation is not enough, there are various community channels -available:

- -
-
- - + + + + + + +SeleniumLibrary + + + + +
+

SeleniumLibrary

+ + +
+

Introduction

+

SeleniumLibrary is a web testing library for Robot Framework that +utilizes the Selenium tool internally. The project is hosted on GitHub +and downloads can be found from PyPI.

+

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.11. +In addition to the normal Python interpreter, it works also +with PyPy.

+

SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to +Selenium2Library and then later renamed back to SeleniumLibrary. +See the VERSIONS.rst for more information about different versions and the +overall project history.

+ +https://img.shields.io/pypi/v/robotframework-seleniumlibrary.svg?label=version + + +https://img.shields.io/pypi/dm/robotframework-seleniumlibrary.svg + + +https://img.shields.io/pypi/l/robotframework-seleniumlibrary.svg + + +https://github.com/robotframework/SeleniumLibrary/actions/workflows/CI.yml/badge.svg?branch=master + +
+
+

Keyword Documentation

+

See keyword documentation for available keywords and more information +about the library in general.

+
+
+

Installation

+

The recommended installation method is using pip:

+
pip install --upgrade robotframework-seleniumlibrary
+

Running this command installs also the latest Selenium and Robot Framework +versions, but you still need to install browser drivers separately. +The --upgrade option can be omitted when installing the library for the +first time.

+

It is possible to install directly from the GitHub repository. To install +latest source from the master branch, use this command:

+
pip install git+https://github.com/robotframework/SeleniumLibrary.git
+

Please note that installation will take some time, because pip will +clone the SeleniumLibrary project to a temporary directory and then +perform the installation.

+

See Robot Framework installation instructions for detailed information +about installing Python and Robot Framework itself. For more details about +using pip see its own documentation.

+
+
+

Browser drivers

+

After installing the library, you still need to install browser and +operating system specific browser drivers for all those browsers you +want to use in tests. These are the exact same drivers you need to use with +Selenium also when not using SeleniumLibrary. More information about +drivers can be found from Selenium documentation.

+

The general approach to install a browser driver is downloading a right +driver, such as chromedriver for Chrome, and placing it into +a directory that is in PATH. Drivers for different browsers +can be found via Selenium documentation or by using your favorite +search engine with a search term like selenium chrome browser driver. +New browser driver versions are released to support features in +new browsers, fix bug, or otherwise, and you need to keep an eye on them +to know when to update drivers you use.

+

Alternatively, you can use a tool called WebdriverManager which can +find the latest version or when required, any version of appropriate +webdrivers for you and then download and link/copy it into right +location. Tool can run on all major operating systems and supports +downloading of Chrome, Firefox, Opera & Edge webdrivers.

+

Here's an example:

+
pip install webdrivermanager
+webdrivermanager firefox chrome --linkpath /usr/local/bin
+
+
+

Usage

+

To use SeleniumLibrary in Robot Framework tests, the library needs to +first be imported using the Library setting as any other library. +The library accepts some import time arguments, which are documented +in the keyword documentation along with all the keywords provided +by the library.

+

When using Robot Framework, it is generally recommended to write as +easy-to-understand tests as possible. The keywords provided by +SeleniumLibrary is pretty low level, though, and often require +implementation-specific arguments like element locators to be passed +as arguments. It is thus typically a good idea to write tests using +Robot Framework's higher-level keywords that utilize SeleniumLibrary +keywords internally. This is illustrated by the following example +where SeleniumLibrary keywords like Input Text are primarily +used by higher-level keywords like Input Username.

+
*** Settings ***
+Documentation     Simple example using SeleniumLibrary.
+Library           SeleniumLibrary
+
+*** Variables ***
+${LOGIN URL}      http://localhost:7272
+${BROWSER}        Chrome
+
+*** Test Cases ***
+Valid Login
+    Open Browser To Login Page
+    Input Username    demo
+    Input Password    mode
+    Submit Credentials
+    Welcome Page Should Be Open
+    [Teardown]    Close Browser
+
+*** Keywords ***
+Open Browser To Login Page
+    Open Browser    ${LOGIN URL}    ${BROWSER}
+    Title Should Be    Login Page
+
+Input Username
+    [Arguments]    ${username}
+    Input Text    username_field    ${username}
+
+Input Password
+    [Arguments]    ${password}
+    Input Text    password_field    ${password}
+
+Submit Credentials
+    Click Button    login_button
+
+Welcome Page Should Be Open
+    Title Should Be    Welcome Page
+

The above example is a slightly modified version of an example in a +demo project that illustrates using Robot Framework and SeleniumLibrary. +See the demo for more examples that you can also execute on your own +machine. For more information about Robot Framework test data syntax in +general see the Robot Framework User Guide.

+
+
+

Extending SeleniumLibrary

+

Before creating your own library which extends the SeleniumLibrary, please consider would +the extension be also useful also for general usage. If it could be useful also for general +usage, please create a new issue describing the enhancement request and even better if the +issue is backed up by a pull request.

+

If the enhancement is not generally useful, example solution is domain specific, then the +SeleniumLibrary offers public APIs which can be used to build its own plugins and libraries. +Plugin API allows us to add new keywords, modify existing keywords and modify the internal +functionality of the library. Also new libraries can be built on top of the +SeleniumLibrary. Please see extending documentation for more details about the +available methods and for examples how the library can be extended.

+
+
+

Community

+

If the provided documentation is not enough, there are various community channels +available:

+ +
+
+ + From e8c16497be2ab1f19984266cbaea4732456dcb2e Mon Sep 17 00:00:00 2001 From: vs-sap <161034139+vs-sap@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:03:07 +0100 Subject: [PATCH 077/122] Remove unneeded 'send_' in docs --- src/SeleniumLibrary/keywords/element.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SeleniumLibrary/keywords/element.py b/src/SeleniumLibrary/keywords/element.py index 131fadbf9..831ebfaf2 100644 --- a/src/SeleniumLibrary/keywords/element.py +++ b/src/SeleniumLibrary/keywords/element.py @@ -923,7 +923,6 @@ def press_key(self, locator: Union[WebElement, str], key: str): using the selenium send_keys method. Although one is not recommended over the other if `Press Key` does not work we recommend trying `Press Keys`. - send_ """ if key.startswith("\\") and len(key) > 1: key = self._map_ascii_key_code_to_key(int(key[1:])) From c52437b2452147b114fd10092c86fe520faf87bd Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Wed, 4 Sep 2024 22:12:43 -0400 Subject: [PATCH 078/122] Initial Python 3.12 support --- .github/workflows/CI.yml | 2 +- README.rst | 2 +- ..._options_string_errors_py3_12.approved.txt | 8 ++++++++ ..._service_string_errors_py3_12.approved.txt | 8 ++++++++ .../keywords/test_selenium_options_parser.py | 19 ++++++++++++++++--- .../keywords/test_selenium_service_parser.py | 19 ++++++++++++++++++- 6 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 utest/test/keywords/approved_files/test_selenium_options_parser.test_parse_options_string_errors_py3_12.approved.txt create mode 100644 utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors_py3_12.approved.txt diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8db0eff25..c5d2008d8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: continue-on-error: true strategy: matrix: - python-version: [3.8, 3.11] # 3.12, pypy-3.9 + python-version: [3.8, 3.12] # 3.12, pypy-3.9 rf-version: [5.0.1, 6.1.1, 7.0] selenium-version: [4.20.0, 4.21.0] browser: [firefox, chrome, headlesschrome] #edge diff --git a/README.rst b/README.rst index 85819fcab..727c8e705 100644 --- a/README.rst +++ b/README.rst @@ -10,7 +10,7 @@ SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes the Selenium_ tool internally. The project is hosted on GitHub_ and downloads can be found from PyPI_. -SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.11. +SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.12. In addition to the normal Python_ interpreter, it works also with PyPy_. diff --git a/utest/test/keywords/approved_files/test_selenium_options_parser.test_parse_options_string_errors_py3_12.approved.txt b/utest/test/keywords/approved_files/test_selenium_options_parser.test_parse_options_string_errors_py3_12.approved.txt new file mode 100644 index 000000000..d57473c22 --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_options_parser.test_parse_options_string_errors_py3_12.approved.txt @@ -0,0 +1,8 @@ +Selenium options string errors + +0) method("arg1) ('unterminated string literal (detected at line 1)', (1, 8)) +1) method(arg1") ('unterminated string literal (detected at line 1)', (1, 12)) +2) method(arg1) Unable to parse option: "method(arg1)" +3) attribute=arg1 Unable to parse option: "attribute=arg1" +4) attribute=webdriver Unable to parse option: "attribute=webdriver" +5) method(argument="value") Unable to parse option: "method(argument="value")" diff --git a/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors_py3_12.approved.txt b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors_py3_12.approved.txt new file mode 100644 index 000000000..44dc032d0 --- /dev/null +++ b/utest/test/keywords/approved_files/test_selenium_service_parser.test_parse_service_string_errors_py3_12.approved.txt @@ -0,0 +1,8 @@ +Selenium service string errors + +0) attribute=arg1 Unable to parse service: "attribute=arg1" +1) attribute='arg1 ('unterminated string literal (detected at line 1)', (1, 11)) +2) attribute=['arg1' ('unexpected EOF in multi-line statement', (1, 0)) +3) attribute=['arg1';'arg2'] ('unexpected EOF in multi-line statement', (1, 0)) +4) attribute['arg1'] Unable to parse service: "attribute['arg1']" +5) attribute=['arg1'] attribute=['arg2'] Unable to parse service: "attribute=['arg1'] attribute=['arg2']" diff --git a/utest/test/keywords/test_selenium_options_parser.py b/utest/test/keywords/test_selenium_options_parser.py index 40e51bdd9..b61fff029 100644 --- a/utest/test/keywords/test_selenium_options_parser.py +++ b/utest/test/keywords/test_selenium_options_parser.py @@ -1,4 +1,5 @@ import os +import sys import unittest import pytest @@ -102,7 +103,8 @@ def test_parse_arguemnts(options, reporter): verify_all("Parse arguments from complex object", results, reporter=reporter) -@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +@pytest.mark.skipif(WINDOWS, reason="ApprovalTest do not support different line feeds") +@pytest.mark.skipif(sys.version_info > (3, 11), reason="Errors change with Python 3.12") def test_parse_options_string_errors(options, reporter): results = [] results.append(error_formatter(options._parse, 'method("arg1)', True)) @@ -114,6 +116,19 @@ def test_parse_options_string_errors(options, reporter): verify_all("Selenium options string errors", results, reporter=reporter) +@pytest.mark.skipif(WINDOWS, reason="ApprovalTest do not support different line feeds") +@pytest.mark.skipif(sys.version_info < (3, 12), reason="Errors change with Python 3.12") +def test_parse_options_string_errors_py3_12(options, reporter): + results = [] + results.append(error_formatter(options._parse, 'method("arg1)', True)) + results.append(error_formatter(options._parse, 'method(arg1")', True)) + results.append(error_formatter(options._parse, "method(arg1)", True)) + results.append(error_formatter(options._parse, "attribute=arg1", True)) + results.append(error_formatter(options._parse, "attribute=webdriver", True)) + results.append(error_formatter(options._parse, 'method(argument="value")', True)) + verify_all("Selenium options string errors", results, reporter=reporter) + + @unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") def test_split_options(options, reporter): results = [] @@ -203,8 +218,6 @@ def output_dir(): output_dir = os.path.abspath(os.path.join(curr_dir, "..", "..", "output_dir")) return output_dir -from selenium.webdriver.chrome.service import Service as ChromeService - def test_create_chrome_with_options(creator): options = mock() diff --git a/utest/test/keywords/test_selenium_service_parser.py b/utest/test/keywords/test_selenium_service_parser.py index 637a208c6..095a8c2c2 100644 --- a/utest/test/keywords/test_selenium_service_parser.py +++ b/utest/test/keywords/test_selenium_service_parser.py @@ -1,4 +1,5 @@ import os +import sys import unittest import pytest @@ -53,7 +54,10 @@ def test_parse_service_string(service, reporter): verify_all("Selenium service string to dict", results, reporter=reporter) -@unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +# @unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") +# @unittest.skipIf(sys.version_info > (3, 11), reason="Errors change with Python 3.12") +@pytest.mark.skipif(WINDOWS, reason="ApprovalTest do not support different line feeds") +@pytest.mark.skipif(sys.version_info > (3, 11), reason="Errors change with Python 3.12") def test_parse_service_string_errors(service, reporter): results = [] results.append(error_formatter(service._parse, "attribute=arg1", True)) @@ -65,6 +69,19 @@ def test_parse_service_string_errors(service, reporter): verify_all("Selenium service string errors", results, reporter=reporter) +@pytest.mark.skipif(WINDOWS, reason="ApprovalTest do not support different line feeds") +@pytest.mark.skipif(sys.version_info < (3, 12), reason="Errors change with Python 3.12") +def test_parse_service_string_errors_py3_12(service, reporter): + results = [] + results.append(error_formatter(service._parse, "attribute=arg1", True)) + results.append(error_formatter(service._parse, "attribute='arg1", True)) + results.append(error_formatter(service._parse, "attribute=['arg1'", True)) + results.append(error_formatter(service._parse, "attribute=['arg1';'arg2']", True)) + results.append(error_formatter(service._parse, "attribute['arg1']", True)) + results.append(error_formatter(service._parse, "attribute=['arg1'] attribute=['arg2']", True)) + verify_all("Selenium service string errors", results, reporter=reporter) + + @unittest.skipIf(WINDOWS, reason="ApprovalTest do not support different line feeds") def test_split_service(service, reporter): results = [] From f263368af53eb8a7ded2b5754ab8fbbe18aa2064 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 5 Sep 2024 08:56:45 -0400 Subject: [PATCH 079/122] Triage atest "Should Detect Page Loads While Waiting On An Async Script And Return An Error" is showing some differences I believe with the current Chrome browser version 128. Going to temporarily triage it and see if there are other issues within the test matrix. --- atest/acceptance/keywords/async_javascript.robot | 1 + 1 file changed, 1 insertion(+) diff --git a/atest/acceptance/keywords/async_javascript.robot b/atest/acceptance/keywords/async_javascript.robot index 7fc72b198..646e3dd16 100644 --- a/atest/acceptance/keywords/async_javascript.robot +++ b/atest/acceptance/keywords/async_javascript.robot @@ -88,6 +88,7 @@ Should Timeout If Script Does Not Invoke Callback With Long Timeout ... var callback = arguments[arguments.length - 1]; window.setTimeout(callback, 1500); Should Detect Page Loads While Waiting On An Async Script And Return An Error + [Tags] Triage Set Selenium Timeout 0.5 seconds ${status} ${error} Run Keyword And Ignore Error Execute Async Javascript ... window.location = 'javascript/dynamic'; From f3f41110389c34543fcf0dfbd492f632243ea10b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 5 Sep 2024 13:30:51 -0400 Subject: [PATCH 080/122] Updated tested against Robot Framework versions Removed 5.0.1 as it is not compatable with Python3.12 (which came in RF version 6.1). Also bumped 7.0 to 7.0.1. --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c5d2008d8..4628a13a9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: python-version: [3.8, 3.12] # 3.12, pypy-3.9 - rf-version: [5.0.1, 6.1.1, 7.0] + rf-version: [6.1.1, 7.0.1] selenium-version: [4.20.0, 4.21.0] browser: [firefox, chrome, headlesschrome] #edge From ff8cfb6ac7742ff11dbf5183c460e94ea2f9c659 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 5 Sep 2024 13:55:50 -0400 Subject: [PATCH 081/122] Updated setup.py with 3.12 support - also cleaned up the github runner --- .github/workflows/CI.yml | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4628a13a9..d61836c57 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -9,7 +9,7 @@ jobs: continue-on-error: true strategy: matrix: - python-version: [3.8, 3.12] # 3.12, pypy-3.9 + python-version: [3.8, 3.12] # pypy-3.9 rf-version: [6.1.1, 7.0.1] selenium-version: [4.20.0, 4.21.0] browser: [firefox, chrome, headlesschrome] #edge diff --git a/setup.py b/setup.py index 8ddead98f..89d86d307 100755 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 +Programming Language :: Python :: 3.12 Programming Language :: Python :: 3 :: Only Topic :: Software Development :: Testing Framework :: Robot Framework @@ -41,7 +42,7 @@ keywords = 'robotframework testing testautomation selenium webdriver web', platforms = 'any', classifiers = CLASSIFIERS, - python_requires = '>=3.8, <3.12', + python_requires = '>=3.8, <=3.12', install_requires = REQUIREMENTS, package_dir = {'': 'src'}, packages = find_packages('src'), From 305b8f6e8654543ca019e8e54b2b77e4965f2ad9 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 5 Sep 2024 14:05:29 -0400 Subject: [PATCH 082/122] Updated Selenium Python versions to latest As I did not keep up with the selenium version over the summer going to test from 4.21.0 to latest (4.24.0) --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d61836c57..2d0bda6c9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,7 +11,7 @@ jobs: matrix: python-version: [3.8, 3.12] # pypy-3.9 rf-version: [6.1.1, 7.0.1] - selenium-version: [4.20.0, 4.21.0] + selenium-version: [4.21.0, 4.22.0, 4.23.1, 4.24.0] browser: [firefox, chrome, headlesschrome] #edge steps: From 948332aa52ffd29ecb9fd55ff2e6420f92654014 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Thu, 5 Sep 2024 14:51:39 -0400 Subject: [PATCH 083/122] Updated the "Install drivers via selenium-manager" CI step As we now are only testing Selenium version 2.21 or greater I removed the check to see the correct method name for getting the binaries with Selenium Manage. --- .github/workflows/CI.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2d0bda6c9..d24a0ba01 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -60,11 +60,7 @@ jobs: pip install -U --pre robotframework==${{ matrix.rf-version }} - name: Install drivers via selenium-manager run: | - if [[ ${{ matrix.selenium-version }} == '4.20.0' || ${{ matrix.selenium-version }} == '4.21.0' ]]; then - SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm._get_binary())}")') - else - SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm.get_binary())}")') - fi + SELENIUM_MANAGER_EXE=$(python -c 'from selenium.webdriver.common.selenium_manager import SeleniumManager; sm=SeleniumManager(); print(f"{str(sm._get_binary())}")') echo "$SELENIUM_MANAGER_EXE" echo "WEBDRIVERPATH=$($SELENIUM_MANAGER_EXE --browser chrome --debug | awk '/INFO[[:space:]]Driver path:/ {print $NF;exit}')" >> "$GITHUB_ENV" echo "$WEBDRIVERPATH" From 27450d96cfea92c9c426bf13b74685625f4d874b Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 6 Sep 2024 15:23:57 -0400 Subject: [PATCH 084/122] Release notes for 6.6.0 --- docs/SeleniumLibrary-6.6.0.rst | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/SeleniumLibrary-6.6.0.rst diff --git a/docs/SeleniumLibrary-6.6.0.rst b/docs/SeleniumLibrary-6.6.0.rst new file mode 100644 index 000000000..5e0b74f42 --- /dev/null +++ b/docs/SeleniumLibrary-6.6.0.rst @@ -0,0 +1,79 @@ +===================== +SeleniumLibrary 6.6.0 +===================== + + +.. default-role:: code + + +SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes +the Selenium_ tool internally. SeleniumLibrary 6.6.0 is a new release which adds +Python 3.12 support. + +If you have pip_ installed, just run + +:: + + pip install --upgrade robotframework-seleniumlibrary + +to install the latest available release or use + +:: + + pip install robotframework-seleniumlibrary==6.6.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +SeleniumLibrary 6.6.0 was released on Friday September 6, 2024. SeleniumLibrary supports +Python 3.8 through 3.12, Selenium 4.21.0 through 4.24.0 and +Robot Framework 6.1.1 and 7.0.1. + +*In addition this version of SeleniumLibrary has been tested against the upcoming Robot +Framework v7.1 release (using v7.1rc2) and was found compatible. We expect it to work +fine with the final release which should be coming out soon.* + +.. _Robot Framework: http://robotframework.org +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium: http://seleniumhq.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary +.. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues?q=milestone%3Av6.6.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +- Support for Python 3.12 was added in this release. In addition we added Robot Framework 7.0.1 + while dropping 5.0.1 which did not officially support Python 3.12. In addition with the almost + monthly releases of Selenium we have caught up testing against and supporting Selenium versions + 4.21.0, 4.22.0, 4.23.1, and 4.24.0. (`#1906`_) + +Acknowledgements +================ + +- I want to thank grepwood, KotlinIsland, and Robin Mackaij for pushing support python 3.12 and + Yuri, Tatu and Lassi for reviewing the changes. (`#1906`_) + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#1906`_ + - enhancement + - high + - Support python 3.12 + +Altogether 1 issue. View on the `issue tracker `__. + +.. _#1906: https://github.com/robotframework/SeleniumLibrary/issues/1906 From acd2b10b637e2913eac91c22944a3fbc3047a500 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 6 Sep 2024 15:24:47 -0400 Subject: [PATCH 085/122] Updated version to 6.6.0 --- src/SeleniumLibrary/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 1b2618941..1ac20b245 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -55,7 +55,7 @@ from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay -__version__ = "6.5.0" +__version__ = "6.6.0" class SeleniumLibrary(DynamicCore): From 70139f3b88611317a34fd817d694d9aa4ab2796d Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 6 Sep 2024 15:25:30 -0400 Subject: [PATCH 086/122] Generate stub file for 6.6.0 --- src/SeleniumLibrary/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.pyi b/src/SeleniumLibrary/__init__.pyi index d17efdd1c..45998d55a 100644 --- a/src/SeleniumLibrary/__init__.pyi +++ b/src/SeleniumLibrary/__init__.pyi @@ -107,7 +107,7 @@ class SeleniumLibrary: def mouse_out(self, locator: Union): ... def mouse_over(self, locator: Union): ... def mouse_up(self, locator: Union): ... - def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Optional] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None, service: Optional[Optional] = None): ... + def open_browser(self, url: Optional[Optional] = None, browser: str = 'firefox', alias: Optional[Optional] = None, remote_url: Union = False, desired_capabilities: Optional[Union] = None, ff_profile_dir: Optional[Union] = None, options: Optional[Any] = None, service_log_path: Optional[Optional] = None, executable_path: Optional[Optional] = None, service: Optional[Any] = None): ... def open_context_menu(self, locator: Union): ... def page_should_contain(self, text: str, loglevel: str = 'TRACE'): ... def page_should_contain_button(self, locator: Union, message: Optional[Optional] = None, loglevel: str = 'TRACE'): ... From 80e610146bd7f64c4f1efa3c686fc6b8ed1e352f Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Fri, 6 Sep 2024 15:25:59 -0400 Subject: [PATCH 087/122] Generated docs for version 6.6.0 --- docs/SeleniumLibrary.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SeleniumLibrary.html b/docs/SeleniumLibrary.html index cf4ebe698..c88cb0cd9 100644 --- a/docs/SeleniumLibrary.html +++ b/docs/SeleniumLibrary.html @@ -7,7 +7,7 @@ - + + + + + + + + + + + + + + +
+

Opening library documentation failed

+
    +
  • Verify that you have JavaScript enabled in your browser.
  • +
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • +
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
+
+ + + + + + + + + + + + + + + + From 0c99a6d6728e4b59035dfcfafe5da6ae49d6e062 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Sun, 29 Dec 2024 12:32:11 -0500 Subject: [PATCH 111/122] Regenerated project docs --- docs/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index 7343120a3..3a9904b77 100644 --- a/docs/index.html +++ b/docs/index.html @@ -29,7 +29,7 @@

Introduction<

SeleniumLibrary is a web testing library for Robot Framework that utilizes the Selenium tool internally. The project is hosted on GitHub and downloads can be found from PyPI.

-

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.12. +

SeleniumLibrary currently works with Selenium 4. It supports Python 3.8 through 3.13. In addition to the normal Python interpreter, it works also with PyPy.

SeleniumLibrary is based on the "old SeleniumLibrary" that was forked to From 3fec008472b1cd845f3a63b129fba2fd96d2c627 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 6 Jan 2025 07:28:11 -0500 Subject: [PATCH 112/122] Release notes for 6.7.0 --- docs/SeleniumLibrary-6.7.0.rst | 106 +++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/SeleniumLibrary-6.7.0.rst diff --git a/docs/SeleniumLibrary-6.7.0.rst b/docs/SeleniumLibrary-6.7.0.rst new file mode 100644 index 000000000..f1bf8d778 --- /dev/null +++ b/docs/SeleniumLibrary-6.7.0.rst @@ -0,0 +1,106 @@ +===================== +SeleniumLibrary 6.7.0 +===================== + + +.. default-role:: code + + +SeleniumLibrary_ is a web testing library for `Robot Framework`_ that utilizes +the Selenium_ tool internally. SeleniumLibrary 6.7.0 is a new release with +some minor enhancements and bug fixes. This versions add support for Python 3.13. + + +If you have pip_ installed, just run + +:: + + pip install --upgrade robotframework-seleniumlibrary + +to install the latest available release or use + +:: + + pip install robotframework-seleniumlibrary==6.7.0 + +to install exactly this version. Alternatively you can download the source +distribution from PyPI_ and install it manually. + +SeleniumLibrary 6.7.0 was released on Monday January 6, 2025. SeleniumLibrary supports +Python 3.8 through 3.13, Selenium 4.24.0 through 4.27.1 and +Robot Framework 6.1.1 and 7.1.1. + +*Note: This release, v 6.7.0, has been tested against the latest release candidate of the +upcoming Robot Framework version, 7.2. It is compatible and expect to support 7.2 when the +final release is made.* + +.. _Robot Framework: http://robotframework.org +.. _SeleniumLibrary: https://github.com/robotframework/SeleniumLibrary +.. _Selenium: http://seleniumhq.org +.. _pip: http://pip-installer.org +.. _PyPI: https://pypi.python.org/pypi/robotframework-seleniumlibrary +.. _issue tracker: https://github.com/robotframework/SeleniumLibrary/issues?q=milestone%3Av6.7.0 + + +.. contents:: + :depth: 2 + :local: + +Most important enhancements +=========================== + +- Fixed _find_by_data_locator when more than one colon was within the locator. If one + used the data strategy and the locator had additional colon in it the locator parser + would incorrectly parse the locator. This has been fixed in this release. (`#1924`_) +- Make SeleniumLibrary support one or more translations from same localisation project (`#1917`_) +- Support for Python version 3.13 + +Acknowledgements +================ + +We want to thank + +- `Markus Leben `_ for discovering, reporting, and fixing + the _find_by_data_locator issue (`#1924`_) +- `The Great Simo `_ and `Pavel `_ + for updating the requirements (`#1849`_) +- `iarmhi `_ for correcting an error the docs (`#1913`_) + +Full list of fixes and enhancements +=================================== + +.. list-table:: + :header-rows: 1 + + * - ID + - Type + - Priority + - Summary + * - `#1924`_ + - bug + - high + - Fix _find_by_data_locator + * - `#1917`_ + - enhancement + - high + - Make SeleniumLibrary support one or more translation from same localisation project + * - `#1849`_ + - --- + - medium + - Update the rerequirements + * - `#1913`_ + - bug + - low + - Remove unneeded 'send_' in docs + * - `#1925`_ + - --- + - --- + - Latest Versions Oct2024 + +Altogether 5 issues. View on the `issue tracker `__. + +.. _#1924: https://github.com/robotframework/SeleniumLibrary/issues/1924 +.. _#1917: https://github.com/robotframework/SeleniumLibrary/issues/1917 +.. _#1849: https://github.com/robotframework/SeleniumLibrary/issues/1849 +.. _#1913: https://github.com/robotframework/SeleniumLibrary/issues/1913 +.. _#1925: https://github.com/robotframework/SeleniumLibrary/issues/1925 From af36f39bdc0a319167c731045e79d5784c1b27f7 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 6 Jan 2025 07:28:39 -0500 Subject: [PATCH 113/122] Updated version to 6.7.0 --- src/SeleniumLibrary/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index a85e2bc8c..1228fb94e 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -55,7 +55,7 @@ from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay -__version__ = "6.7.0rc1" +__version__ = "6.7.0" class SeleniumLibrary(DynamicCore): From 68b81495dc64fda6f5fa7a7359f199581404fd61 Mon Sep 17 00:00:00 2001 From: Ed Manlove Date: Mon, 6 Jan 2025 07:29:54 -0500 Subject: [PATCH 114/122] Generated docs for version 6.7.0 --- docs/SeleniumLibrary.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SeleniumLibrary.html b/docs/SeleniumLibrary.html index 92c67b30f..2bd5eac76 100644 --- a/docs/SeleniumLibrary.html +++ b/docs/SeleniumLibrary.html @@ -7,7 +7,7 @@ - + - - - - - - - + - - - + +

Opening library documentation failed

  • Verify that you have JavaScript enabled in your browser.
  • -
  • Make sure you are using a modern enough browser. If using Internet Explorer, version 11 is required.
  • -
  • Check are there messages in your browser's JavaScript error log. Please report the problem if you suspect you have encountered a bug.
  • +
  • + Make sure you are using a modern enough browser. If using + Internet Explorer, version 11 is required. +
  • +
  • + Check are there messages in your browser's + JavaScript error log. Please report the problem if you suspect + you have encountered a bug. +
- - - + + + + +
+ - + + + + + + - + - - - + + - + + - - - - - + {{#if usages.length}} +
+

{{t "usages"}}

+
    + {{#each usages}} +
  • {{this}}
  • + {{/each}} +
+
+ {{/if}} +

+
+ + + From 15ce0966638cb3a2e7e22fa4cc7fd129030252d8 Mon Sep 17 00:00:00 2001 From: Corey Goldberg <1113081+cgoldberg@users.noreply.github.com> Date: Fri, 23 May 2025 10:50:47 -0400 Subject: [PATCH 122/122] Update README.rst 'Browser drivers' section --- README.rst | 37 ++++--------------------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/README.rst b/README.rst index efd83b3c4..1f4346024 100644 --- a/README.rst +++ b/README.rst @@ -44,8 +44,7 @@ The recommended installation method is using pip_:: pip install --upgrade robotframework-seleniumlibrary Running this command installs also the latest Selenium and Robot Framework -versions, but you still need to install `browser drivers`_ separately. -The ``--upgrade`` option can be omitted when installing the library for the +versions. The ``--upgrade`` option can be omitted when installing the library for the first time. It is possible to install directly from the GitHub_ repository. To install @@ -64,39 +63,11 @@ using ``pip`` see `its own documentation `__. Browser drivers --------------- -After installing the library, you still need to install browser and -operating system specific browser drivers for all those browsers you -want to use in tests. These are the exact same drivers you need to use with -Selenium also when not using SeleniumLibrary. More information about -drivers can be found from `Selenium documentation`__. - -The general approach to install a browser driver is downloading a right -driver, such as ``chromedriver`` for Chrome, and placing it into -a directory that is in PATH__. Drivers for different browsers -can be found via Selenium documentation or by using your favorite -search engine with a search term like ``selenium chrome browser driver``. -New browser driver versions are released to support features in -new browsers, fix bug, or otherwise, and you need to keep an eye on them -to know when to update drivers you use. - -Alternatively, you can use a tool called WebdriverManager__ which can -find the latest version or when required, any version of appropriate -webdrivers for you and then download and link/copy it into right -location. Tool can run on all major operating systems and supports -downloading of Chrome, Firefox, Opera & Edge webdrivers. - -Here's an example: - -.. code:: bash - - pip install webdrivermanager - webdrivermanager firefox chrome --linkpath /usr/local/bin - - +Browsers and drivers are installed and managed automatically by `Selenium Manager`__. +For more information, see the `Selenium documentation`__. +__ https://www.selenium.dev/documentation/selenium_manager __ https://seleniumhq.github.io/selenium/docs/api/py/index.html#drivers -__ https://en.wikipedia.org/wiki/PATH_(variable) -__ https://github.com/omenia/webdrivermanager Usage -----