Repository: robotframework/SeleniumLibrary
Pull request #1865 (2023-11-11 03:54:08)

Add expected conditions 1827
Some very early and very basic sketches of what this might become ..


Patch:
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index defc9fd9..23e3a3e2 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/src/SeleniumLibrary/__init__.py b/src/SeleniumLibrary/__init__.py
index 2788d2c6..6211c3eb 100644
--- a/src/SeleniumLibrary/__init__.py
+++ b/src/SeleniumLibrary/__init__.py
@@ -35,6 +35,7 @@ from SeleniumLibrary.keywords import (
     BrowserManagementKeywords,
     CookieKeywords,
     ElementKeywords,
+    ExpectedConditionKeywords,
     FormElementKeywords,
     FrameKeywords,
     JavaScriptKeywords,
@@ -490,6 +491,7 @@ class SeleniumLibrary(DynamicCore):
             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 68ed5ab3..5dd4310d 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 184efff1..fc9f357c 100644
--- a/src/SeleniumLibrary/keywords/__init__.py
+++ b/src/SeleniumLibrary/keywords/__init__.py
@@ -18,6 +18,7 @@ from .alert import AlertKeywords  # noqa
 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 00000000..7b896fd6
--- /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


Test patch:
diff --git a/atest/acceptance/expected_conditions.robot.PROTOTYPE b/atest/acceptance/expected_conditions.robot.PROTOTYPE
new file mode 100644
index 00000000..06c47185
--- /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 89dc975f..c03b1275 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 4464ad87..d1fd4d4b 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 00000000..4fa40dbf
--- /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 6555267c..a39df7d5 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 14dfde44..40ce798d 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 284d5d4e..8c602cd6 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);
+		}
 		</script>
 	</head>
 	<body>
 		<a href="javascript:return false;" onclick="document.title='Changed'; return false;">change title</a><br/>
+		<a href="javascript:return false;" onclick="delayed_title_change(); return false;">delayed change title</a><br/>
 		<a href="javascript:return false;" onclick="add_content('target', 'added content'); return false;">add content</a><br/>
 		<a id="unicode" href="javascript:return false;" onclick="document.title='äää'; return false;">title to ääää</a><br/>
 		<p>
diff --git a/atest/resources/html/javascript/expected_conditions.html b/atest/resources/html/javascript/expected_conditions.html
new file mode 100644
index 00000000..621421e8
--- /dev/null
+++ b/atest/resources/html/javascript/expected_conditions.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+		<title>Original</title>
+		<!-- Note the timeouts in the functions are chosen such they constitute
+		     a set with distinct subset sums. That is is a combination of any two
+		     or more were to be executed the total time for that distinct combination
+		     would be unique. This was done so we could do a ALL/AND/OR type of test
+		     and know that with the combination of expected conditions the execution
+		     was successful as the unique time took place. -->
+		<!-- The set we are using is {6,9,11,12,13} -->
+		<!-- (could also use {3,5,6,7} which would reduce total time)-->
+		<script type="text/javascript">
+		function add_content(target_container, content) {
+			p = document.createElement('p');
+			p.appendChild(document.createTextNode(content));
+			container = document.getElementById(target_container);
+			container.appendChild(p);
+		}
+
+		function delayed_button_state() {
+			setTimeout(function() {
+				state_btn = document.getElementById('enabledDisabledBtn')
+				if (state_btn.hasAttribute("disabled")) {
+					state_btn.removeAttribute("disabled")
+					state_btn.value = "Enabled"
+				} else {
+					state_btn.setAttribute("disabled", "")
+					state_btn.value = "Disabled"
+				}
+			},900)
+		}
+		function delayed_title_change() {
+			setTimeout(function(){
+				document.title='Delayed';
+			},600);
+		}
+
+		function delayed_add_element() {
+		    setTimeout(function(){
+		        const newElem = document.createElement("input");
+		        newElem.setAttribute("type", "button");
+		        newElem.setAttribute("id", "added_btn");
+		        newElem.setAttribute("value", "Added Button");
+		        const container = document.getElementById("container");
+		        document.body.insertBefore(newElem, container);
+		    },1100);
+		}
+		</script>
+	</head>
+	<body>
+		<a href="javascript:return false;" onclick="document.title='Changed'; return false;">change title</a><br/>
+		<a href="javascript:return false;" onclick="delayed_title_change(); return false;">delayed change title</a><br/>
+		<a href="javascript:return false;" onclick="delayed_add_element(); return fales;">delayed add element</a><br/>
+		<a href="javascript:return false;" onclick="add_content('target', 'added content'); return false;">add content</a><br/>
+		<a id="unicode" href="javascript:return false;" onclick="document.title='äää'; return false;">title to ääää</a><br/>
+		<p>
+			<input type="radio" name="group" value="title" 
+			onclick="document.title='Changed by Button';" />Change Title<br/>
+			<input type="radio" name="group" value="content" 
+			onclick="add_content('button_target', 'added by button');"/>Add Content<br/>
+        </p>
+        <div id="target">
+		</div>
+		<div id="button_target">
+        </div>
+        <form name=myform>
+            <input type=button value="Change the title" 
+            onClick="if(confirm('Really change the title?'))
+            document.title += ' Changed!';" >
+		</form>
+		<p>
+			<input type=button id=stateChangeBtn value="Change the button state"
+			onClick="delayed_button_state()" />
+			<input type=button id=enabledDisabledBtn value="Disabled"
+			disabled />
+		</p>
+		<p>
+			<div id="container"></div>
+		</p>
+		<p>
+			<form name=titleChanger>
+				<td>
+					<input type=text id=titleChangeTxt value="Enter Title here">
+				</td>
+				<td>
+					<input type=button id=titleChangeBtn value="Set Title" 
+					onClick="document.title = document.getElementById('titleChangeTxt').value;" >
+				</td>
+			</form>
+		</p>
+	</body>
+</html>
+
diff --git a/atest/run.py b/atest/run.py
index e4059716..2c86cf99 100755
--- a/atest/run.py
+++ b/atest/run.py
@@ -52,6 +52,7 @@ import tempfile
 
 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 @@ if __name__ == "__main__":
         interpreter, browser, rf_options, selenium_grid, event_firing_webdriver
     )
     if args.zip:
-        create_zip()
+        create_zip(browser)
     sys.exit(failures)
diff --git a/utest/test/api/test_plugins.py b/utest/test/api/test_plugins.py
index ea92654f..2a07a115 100644
--- a/utest/test/api/test_plugins.py
+++ b/utest/test/api/test_plugins.py
@@ -22,7 +22,7 @@ class ExtendingSeleniumLibrary(unittest.TestCase):
     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 00000000..3ade2e5f
--- /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 23edcfbf..3a7e895e 100644
--- a/utest/test/keywords/test_firefox_profile_parsing.py
+++ b/utest/test/keywords/test_firefox_profile_parsing.py
@@ -56,8 +56,21 @@ class FireFoxProfileParsingTests(unittest.TestCase):
 
     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
+        # --