diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index defc9fd99..23e3a3e2d 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.0, 4.18.0 + selenium-version: [4.14.0, 4.15.2, 4.16.0, 4.17.2, 4.18.1, 4.19.0] browser: [firefox, chrome, headlesschrome] #edge steps: @@ -88,7 +88,7 @@ jobs: # xvfb-run --auto-servernum python atest/run.py --zip headlesschrome --grid True - uses: actions/upload-artifact@v1 - if: success() || failure() + if: failure() with: name: SeleniumLibrary Test results path: atest/zip_results diff --git a/atest/acceptance/expected_conditions.robot.PROTOTYPE b/atest/acceptance/expected_conditions.robot.PROTOTYPE new file mode 100644 index 000000000..06c471857 --- /dev/null +++ b/atest/acceptance/expected_conditions.robot.PROTOTYPE @@ -0,0 +1,34 @@ +*** Test Cases *** +# Wait Until Element State Is (Not) +# Get Element State +# Element States Should (Not) Be + +Check waiting for condition that takes a element + Fail + +Check waiting for condition that takes a title + Fail + +Check waiting for condition that takes a url + Fail + Wait Until url contains google + # verify took 2 seconds + +Check waiting for condition that takes locator and string + Wait Until Element State Is ${condition} ${locator} ${string} + Wait Until Element State Is ${condition} ${element} + Wait Until Condition Is ${condition} ${target} + Wait Until Condition Is ${condition} ${whatelse you need for this condition} + + + Wait Until State Is number_of_windows_to_be + Wait Until Expected Condition Is number_of_windows_to_be + Wait Until Condition Is number of windows to be 5 + Wait Until Condition Is text to be present in element attribute //some/xpath/to/an/element href http://hello + + Wait Until Condition Is number of windows to be 5 text to be present in element attribute //some/xpath/to/an/element href http://hello + + Wait Until number of windows to be 5 + Wait Until text to be present in element attribute //some/xpath/to/an/element href http://hello + Get Condition + Is number of windows to be 5 \ No newline at end of file diff --git a/atest/acceptance/keywords/choose_file.robot b/atest/acceptance/keywords/choose_file.robot index 89dc975fd..c03b12751 100644 --- a/atest/acceptance/keywords/choose_file.robot +++ b/atest/acceptance/keywords/choose_file.robot @@ -45,11 +45,11 @@ Choose File With Grid From Library Using SL choose_file method Input Text Should Work Same Way When Not Using Grid [Documentation] - ... LOG 1:6 DEBUG GLOB: POST*/session/*/clear {* - ... LOG 1:9 DEBUG Finished Request - ... LOG 1:10 DEBUG GLOB: POST*/session/*/value*"text": "* - ... LOG 1:13 DEBUG Finished Request - ... LOG 1:14 DEBUG NONE + ... LOG 1:6 DEBUG GLOB: POST*/session/*/clear {* + ... LOG 1:9 DEBUG Finished Request + ... LOG 1:10 DEBUG REGEXP: POST.*/session/.*/value.*['\\\"]text['\\\"]: ['\\\"].* + ... LOG 1:13 DEBUG Finished Request + ... LOG 1:14 DEBUG NONE [Tags] NoGrid [Setup] Touch ${CURDIR}${/}temp.txt Input Text file_to_upload ${CURDIR}${/}temp.txt diff --git a/atest/acceptance/keywords/click_element.robot b/atest/acceptance/keywords/click_element.robot index 4464ad874..d1fd4d4b3 100644 --- a/atest/acceptance/keywords/click_element.robot +++ b/atest/acceptance/keywords/click_element.robot @@ -40,7 +40,7 @@ Click Element Action Chain [Tags] NoGrid [Documentation] ... LOB 1:1 INFO Clicking 'singleClickButton' using an action chain. - ... LOG 1:6 DEBUG GLOB: *actions {"actions": [{* + ... LOG 1:6 DEBUG REGEXP: .*actions {['\\\"]actions['\\\"]: \\\[\\\{.* Click Element singleClickButton action_chain=True Element Text Should Be output single clicked diff --git a/atest/acceptance/keywords/expected_conditions.robot b/atest/acceptance/keywords/expected_conditions.robot new file mode 100644 index 000000000..4fa40dbfe --- /dev/null +++ b/atest/acceptance/keywords/expected_conditions.robot @@ -0,0 +1,52 @@ +*** Settings *** +Test Setup Go To Page "javascript/expected_conditions.html" +Resource ../resource.robot + +*** Test Cases *** +Wait For Expected Conditions One Argument + Title Should Be Original + Click Element link=delayed change title + Wait For Expected Condition title_is Delayed + Title Should Be Delayed + +Wait For Expected Condition Times out within set timeout + [Documentation] FAIL REGEXP: TimeoutException: Message: Expected Condition not met within set timeout of 0.3* + Title Should Be Original + Click Element link=delayed change title + Wait For Expected Condition title_is Delayed timeout=0.3 + +Wait For Expected Conditions using WebElement as locator + Click Button Change the button state + ${dynamic_btn}= Get WebElement id:enabledDisabledBtn + Wait For Expected Condition element_to_be_clickable ${dynamic_btn} + +Wait For Expected Conditions Where Condition Written With Spaces + Title Should Be Original + Click Element link=delayed change title + Wait For Expected Condition title is Delayed + Title Should Be Delayed + +Wait For Expected Conditions Where Condition Is Variable + ${condition}= Set Variable title is + Title Should Be Original + Click Element link=delayed change title + Wait For Expected Condition ${condition} Delayed + Title Should Be Delayed + +Wait For Expected Conditions Where Condition Is Strange Case + Click Button Change the button state + ${dynamic_btn}= Get WebElement id:enabledDisabledBtn + Wait For Expected Condition EleMENT tO BE cLiCkAbLe ${dynamic_btn} + +Wait For Non Existing Expected Conditions + Click Button Change the button state + ${dynamic_btn}= Get WebElement id:enabledDisabledBtn + Run Keyword And Expect Error this_is_not_an_expected_con_dition is an unknown expected condition + ... Wait For Expected Condition this_is not an expected con dition ${dynamic_btn} + +Wait For Expected Conditions When Condition Includes Locator + Title Should Be Original + ${byElem}= Evaluate ("id","added_btn") + Click Element link:delayed add element + Wait For Expected Condition Presence Of Element Located ${byElem} + Click Element id:added_btn \ No newline at end of file diff --git a/atest/acceptance/keywords/page_load_timeout.robot b/atest/acceptance/keywords/page_load_timeout.robot index 6555267c1..a39df7d50 100644 --- a/atest/acceptance/keywords/page_load_timeout.robot +++ b/atest/acceptance/keywords/page_load_timeout.robot @@ -7,7 +7,7 @@ Test Teardown Close Browser And Reset Page Load Timeout *** Test Cases *** Should Open Browser With Default Page Load Timeout [Documentation] Verify that 'Open Browser' changes the page load timeout. - ... LOG 1.1.1:27 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {"pageLoad": 300000} + ... LOG 1.1.1:27 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {['\\\"]pageLoad['\\\"]: 300000} ... LOG 1.1.1:29 DEBUG STARTS: Remote response: status=200 # Note: previous log check was 33 and 37. Recording to see if something is swtiching back and forth Open Browser To Start Page @@ -21,8 +21,8 @@ Should Run Into Timeout Exception Should Set Page Load Timeout For All Opened Browsers [Documentation] One browser is already opened as global suite setup. - ... LOG 2:1 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {"pageLoad": 5000} - ... LOG 2:5 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {"pageLoad": 5000} + ... LOG 2:1 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {['\\\"]pageLoad['\\\"]: 5000} + ... LOG 2:5 DEBUG REGEXP: POST http://localhost:\\d{2,5}/session/[a-f0-9-]+/timeouts {['\\\"]pageLoad['\\\"]: 5000} Open Browser To Start Page Set Selenium Page Load Timeout 5 s diff --git a/atest/acceptance/multiple_browsers_options.robot b/atest/acceptance/multiple_browsers_options.robot index 14dfde440..40ce798d7 100644 --- a/atest/acceptance/multiple_browsers_options.robot +++ b/atest/acceptance/multiple_browsers_options.robot @@ -9,32 +9,32 @@ Documentation Creating test which would work on all browser is not possible. *** Test Cases *** Chrome Browser With Selenium Options As String [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 1:14 DEBUG GLOB: *args": ["--disable-dev-shm-usage"?* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument("--disable-dev-shm-usage") Chrome Browser With Selenium Options As String With Attribute As True [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"* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]--headless=new['\\\"].* Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} ... desired_capabilities=${DESIRED_CAPABILITIES} options=add_argument ( "--disable-dev-shm-usage" ) ; add_argument ( "--headless=new" ) 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"?* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]mobileEmulation['\\\"]: {['\\\"]deviceName['\\\"]: ['\\\"]Galaxy S5['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} ... desired_capabilities=${DESIRED_CAPABILITIES} 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"?* + ... LOG 2:14 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* + ... LOG 2:14 DEBUG REGEXP: .*args['\\\"]: \\\[['\\\"]--disable-dev-shm-usage['\\\"].* ${options} = Get Chrome Options Open Browser ${FRONT PAGE} ${BROWSER} remote_url=${REMOTE_URL} ... desired_capabilities=${DESIRED_CAPABILITIES} options=${options} @@ -47,8 +47,8 @@ Chrome Browser With Selenium Options Invalid Method Chrome Browser With Selenium Options Argument With Semicolon [Documentation] - ... LOG 1:14 DEBUG GLOB: *"goog:chromeOptions"* - ... LOG 1:14 DEBUG GLOB: *["has;semicolon"* + ... LOG 1:14 DEBUG REGEXP: .*['\\\"]goog:chromeOptions['\\\"].* + ... LOG 1:14 DEBUG REGEXP: .*\\\[['\\\"]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/html/javascript/dynamic_content.html b/atest/resources/html/javascript/dynamic_content.html index 284d5d4ee..8c602cd64 100644 --- a/atest/resources/html/javascript/dynamic_content.html +++ b/atest/resources/html/javascript/dynamic_content.html @@ -10,10 +10,17 @@ container = document.getElementById(target_container); container.appendChild(p); } + + function delayed_title_change() { + setTimeout(function(){ + document.title='Delayed'; + },600); + } change title
+ delayed change title
add content
title to ääää

diff --git a/atest/resources/html/javascript/expected_conditions.html b/atest/resources/html/javascript/expected_conditions.html new file mode 100644 index 000000000..621421e83 --- /dev/null +++ b/atest/resources/html/javascript/expected_conditions.html @@ -0,0 +1,95 @@ + + + + + Original + + + + + + + change title
+ delayed change title
+ delayed add element
+ add content
+ title to ääää
+

+ Change Title
+ Add Content
+

+
+
+
+
+
+ +
+

+ + +

+

+

+

+

+

+ + + + + + +
+

+ + + diff --git a/atest/run.py b/atest/run.py index e4059716f..2c86cf994 100755 --- a/atest/run.py +++ b/atest/run.py @@ -52,6 +52,7 @@ from robot import rebot_cli from robot import __version__ as robot_version +from selenium import __version__ as selenium_version from robot.utils import is_truthy try: @@ -251,15 +252,15 @@ def process_output(browser): return exit.code -def create_zip(): +def create_zip(browser = None): if os.path.exists(ZIP_DIR): shutil.rmtree(ZIP_DIR) os.mkdir(ZIP_DIR) python_version = platform.python_version() - zip_name = f"rf-{robot_version}-python-{python_version}.zip" + zip_name = f"rf-{robot_version}-python-{python_version}-selenium-{selenium_version}-{browser}.zip" zip_path = os.path.join(ZIP_DIR, zip_name) print("Zip created in: %s" % zip_path) - zip_file = zipfile.ZipFile(zip_path, "w") + zip_file = zipfile.ZipFile(zip_path, "a") for root, dirs, files in os.walk(RESULTS_DIR): for file in files: file_path = os.path.join(root, file) @@ -326,5 +327,5 @@ def create_zip(): interpreter, browser, rf_options, selenium_grid, event_firing_webdriver ) if args.zip: - create_zip() + create_zip(browser) sys.exit(failures) diff --git a/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py index 2788d2c60..6211c3ebd 100644 --- a/src/SeleniumLibrary/__init__.py +++ b/src/SeleniumLibrary/__init__.py @@ -35,6 +35,7 @@ BrowserManagementKeywords, CookieKeywords, ElementKeywords, + ExpectedConditionKeywords, FormElementKeywords, FrameKeywords, JavaScriptKeywords, @@ -490,6 +491,7 @@ def __init__( BrowserManagementKeywords(self), CookieKeywords(self), ElementKeywords(self), + ExpectedConditionKeywords(self), FormElementKeywords(self), FrameKeywords(self), JavaScriptKeywords(self), diff --git a/src/SeleniumLibrary/errors.py b/src/SeleniumLibrary/errors.py index 68ed5ab3f..5dd4310d1 100644 --- a/src/SeleniumLibrary/errors.py +++ b/src/SeleniumLibrary/errors.py @@ -37,3 +37,7 @@ class NoOpenBrowser(SeleniumLibraryException): class PluginError(SeleniumLibraryException): pass + + +class UnkownExpectedCondition(SeleniumLibraryException): + pass \ No newline at end of file diff --git a/src/SeleniumLibrary/keywords/__init__.py b/src/SeleniumLibrary/keywords/__init__.py index 184efff14..fc9f357cf 100644 --- a/src/SeleniumLibrary/keywords/__init__.py +++ b/src/SeleniumLibrary/keywords/__init__.py @@ -18,6 +18,7 @@ from .browsermanagement import BrowserManagementKeywords # noqa from .cookie import CookieKeywords # noqa from .element import ElementKeywords # noqa +from .expectedconditions import ExpectedConditionKeywords # noqa from .formelement import FormElementKeywords # noqa from .frames import FrameKeywords # noqa from .javascript import JavaScriptKeywords # noqa diff --git a/src/SeleniumLibrary/keywords/expectedconditions.py b/src/SeleniumLibrary/keywords/expectedconditions.py new file mode 100644 index 000000000..7b896fd6c --- /dev/null +++ b/src/SeleniumLibrary/keywords/expectedconditions.py @@ -0,0 +1,63 @@ +# 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. +import string +from typing import Optional + +from SeleniumLibrary.base import LibraryComponent, keyword +from SeleniumLibrary.errors import UnkownExpectedCondition +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +class ExpectedConditionKeywords(LibraryComponent): + @keyword + def wait_for_expected_condition(self, condition: string, *args, timeout: Optional[float]=10): + """Waits until ``condition`` is true or ``timeout`` expires. + + 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] + 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 + be passed along after the expected condition. + + Fails if the timeout expires before the condition becomes true. + The default value is 10 seconds. + + Examples: + | `Wait For Expected Condition` | alert_is_present | + | `Wait For Expected Condition` | Title Is | New Title | + + If the expected condition expects a locator then one can pass + as arguments a tuple containing the selenium locator strategies + and the locator. + + Example of expected condition expecting locator: + | ${byElem}= | Evaluate ("id","added_btn") + | `Wait For Expected Condition` | Presence Of Element Located | ${byElem} + """ + + condition = self._parse_condition(condition) + wait = WebDriverWait(self.driver, timeout, 0.1) + try: + c = getattr(EC, condition) + except: + # ToDo: provide hints as to what is avaialbel or find closet match + raise UnkownExpectedCondition(f"{condition} is an unknown expected condition") + result = wait.until(c(*args), message="Expected Condition not met within set timeout of " + str(timeout)) + return result + + def _parse_condition(self, condition: string): + parsed = condition.replace(' ','_').lower() + return parsed \ No newline at end of file diff --git a/utest/test/api/test_plugins.py b/utest/test/api/test_plugins.py index ea92654fa..2a07a1156 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()), 180) + self.assertEqual(len(sl.get_keyword_names()), 181) def test_parse_library(self): plugin = "path.to.MyLibrary" diff --git a/utest/test/keywords/test_expectedconditions.py b/utest/test/keywords/test_expectedconditions.py new file mode 100644 index 000000000..3ade2e5fa --- /dev/null +++ b/utest/test/keywords/test_expectedconditions.py @@ -0,0 +1,31 @@ +import unittest + +from SeleniumLibrary.keywords import ExpectedConditionKeywords + +# Test cases + +# Parsing expected condition +# expect to match .. +# element_to_be_clickable +# Element To Be Clickable +# eLEment TO be ClIcKable +# expect to not match .. +# element__to_be_clickable +# elementtobeclickable +# element_to_be_clickble +# Ice Cream Cone Has Three Scopes + +# what about ..? +# ${ec_var} +# Element\ To\ Be\ Clickable +# Element${SPACE}To${SPACE}Be${SPACE}Clickable + +class ExpectedConditionKeywords(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.ec_keywords = ExpectedConditionKeywords(None) + + def WorkInProgresstest_parse_condition(self): + results = [] + results.append(self.ec_keywords._parse_condition("Element To Be Clickable")) + results.append(self.ec_keywords._parse_condition("eLEment TO be ClIcKable")) diff --git a/utest/test/keywords/test_firefox_profile_parsing.py b/utest/test/keywords/test_firefox_profile_parsing.py index 23edcfbf8..3a7e895e2 100644 --- a/utest/test/keywords/test_firefox_profile_parsing.py +++ b/utest/test/keywords/test_firefox_profile_parsing.py @@ -56,8 +56,21 @@ def test_single_method(self): def _parse_result(self, result): to_str = "" - if "key1" in result.default_preferences: - to_str = f"{to_str} key1 {result.default_preferences['key1']}" - if "key2" in result.default_preferences: - to_str = f"{to_str} key2 {result.default_preferences['key2']}" + result_attr = self._get_preferences_attribute(result) + if "key1" in result_attr: + to_str = f"{to_str} key1 {result_attr['key1']}" + if "key2" in result_attr: + to_str = f"{to_str} key2 {result_attr['key2']}" self.results.append(to_str) + + def _get_preferences_attribute(self, result): + # -- temporary fix to transition selenium to v4.17.2 from v4.16.0 and prior + # from inspect import signature + # sig = signature(result) + if hasattr(result,'default_preferences'): + return result.default_preferences + elif hasattr(result,'_desired_preferences'): + return result._desired_preferences + else: + return None + # --