Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users can now modify ActionChains() duration. #1812

Merged
merged 1 commit into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/SeleniumLibrary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
)
from SeleniumLibrary.keywords.screenshot import EMBED
from SeleniumLibrary.locators import ElementFinder
from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout
from SeleniumLibrary.utils import LibraryListener, is_truthy, _convert_timeout, _convert_delay


__version__ = "6.1.0.dev1"
Expand Down Expand Up @@ -431,6 +431,7 @@ def __init__(
self,
timeout=timedelta(seconds=5),
implicit_wait=timedelta(seconds=0),
action_chain_delay=timedelta(seconds=0.25),
run_on_failure="Capture Page Screenshot",
screenshot_root_directory: Optional[str] = None,
plugins: Optional[str] = None,
Expand All @@ -442,6 +443,8 @@ def __init__(
Default value for `timeouts` used with ``Wait ...`` keywords.
- ``implicit_wait``:
Default value for `implicit wait` used when locating elements.
- ``action_chain_delay``:
Default value for `ActionChains` delay to wait in between actions.
- ``run_on_failure``:
Default action for the `run-on-failure functionality`.
- ``screenshot_root_directory``:
Expand All @@ -456,6 +459,7 @@ def __init__(
"""
self.timeout = _convert_timeout(timeout)
self.implicit_wait = _convert_timeout(implicit_wait)
self.action_chain_delay = _convert_delay(action_chain_delay)
self.speed = 0.0
self.run_on_failure_keyword = RunOnFailureKeywords.resolve_keyword(
run_on_failure
Expand Down
2 changes: 1 addition & 1 deletion src/SeleniumLibrary/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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[Union[str, None]] = None, plugins: Optional[Union[str, None]] = None, event_firing_webdriver: Optional[Union[str, None]] = None): ...
def __init__(self, timeout = timedelta(seconds=5.0), implicit_wait = timedelta(seconds=0.0), action_chain_delay(seconds=0.25)), run_on_failure = 'Capture Page Screenshot', screenshot_root_directory: Optional[Union[str, None]] = None, plugins: Optional[Union[str, None]] = None, event_firing_webdriver: Optional[Union[str, None]] = None): ...
def add_cookie(self, name: str, value: str, path: Optional[Union[str, None]] = None, domain: Optional[Union[str, None]] = None, secure: Optional[Union[bool, None]] = None, expiry: Optional[Union[str, None]] = 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[Union[datetime.timedelta, None]] = None): ...
Expand Down
24 changes: 23 additions & 1 deletion src/SeleniumLibrary/keywords/browsermanagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from SeleniumLibrary.base import keyword, LibraryComponent
from SeleniumLibrary.locators import WindowManager
from SeleniumLibrary.utils import secs_to_timestr, _convert_timeout
from SeleniumLibrary.utils import timestr_to_secs, secs_to_timestr, _convert_timeout, _convert_delay

from .webdrivertools import WebDriverCreator

Expand Down Expand Up @@ -692,6 +692,28 @@ def set_selenium_implicit_wait(self, value: timedelta) -> str:
driver.implicitly_wait(self.ctx.implicit_wait)
return old_wait

@keyword
def set_action_chain_delay(self, value: timedelta) -> str:
"""Sets the duration of delay in ActionChains() used by SeleniumLibrary.

The value can be given as a number that is considered to be
seconds or as a human-readable string like ``1 second``.

Value is always stored as milliseconds internally.

The previous value is returned and can be used to restore
the original value later if needed.
"""
old_action_chain_delay = self.ctx.action_chain_delay
self.ctx.action_chain_delay = _convert_delay(value)
return timestr_to_secs(f"{old_action_chain_delay} milliseconds")

@keyword
def get_action_chain_delay(self):
"""Gets the currently stored value for chain_delay_value in timestr format.
"""
return timestr_to_secs(f"{self.ctx.action_chain_delay} milliseconds")

@keyword
def set_browser_implicit_wait(self, value: timedelta):
"""Sets the implicit wait value used by Selenium.
Expand Down
32 changes: 16 additions & 16 deletions src/SeleniumLibrary/keywords/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ def click_element(

def _click_with_action_chain(self, locator: Union[WebElement, str]):
self.info(f"Clicking '{locator}' using an action chain.")
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
Expand All @@ -672,7 +672,7 @@ def _click_with_modifier(self, locator, tag, modifier):
f"Clicking {tag if tag[0] else 'element'} '{locator}' with {modifier}."
)
modifier = self.parse_modifier(modifier)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
for item in modifier:
action.key_down(item)
element = self.find_element(locator, tag=tag[0], required=False)
Expand Down Expand Up @@ -703,7 +703,7 @@ def click_element_at_coordinates(
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.move_to_element(element)
action.move_by_offset(xoffset, yoffset)
action.click()
Expand All @@ -720,7 +720,7 @@ def double_click_element(self, locator: Union[WebElement, str]):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.double_click(element).perform()

@keyword
Expand All @@ -747,7 +747,7 @@ def scroll_element_into_view(self, locator: Union[WebElement, str]):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
ActionChains(self.driver).move_to_element(element).perform()
ActionChains(self.driver, duration=self.ctx.action_chain_delay).move_to_element(element).perform()

@keyword
def drag_and_drop(
Expand All @@ -768,7 +768,7 @@ def drag_and_drop(
target = self.find_element(target)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
target = _unwrap_eventfiring_element(target)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.drag_and_drop(element, target).perform()

@keyword
Expand All @@ -789,7 +789,7 @@ def drag_and_drop_by_offset(
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.drag_and_drop_by_offset(element, xoffset, yoffset)
action.perform()

Expand All @@ -809,7 +809,7 @@ def mouse_down(self, locator: Union[WebElement, str]):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.click_and_hold(element).perform()

@keyword
Expand All @@ -826,7 +826,7 @@ def mouse_out(self, locator: Union[WebElement, str]):
size = element.size
offsetx = (size["width"] / 2) + 1
offsety = (size["height"] / 2) + 1
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.move_to_element(element)
action.move_by_offset(offsetx, offsety)
action.perform()
Expand All @@ -842,7 +842,7 @@ def mouse_over(self, locator: Union[WebElement, str]):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.move_to_element(element).perform()

@keyword
Expand All @@ -856,15 +856,15 @@ def mouse_up(self, locator: Union[WebElement, str]):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
ActionChains(self.driver).release(element).perform()
ActionChains(self.driver, duration=self.ctx.action_chain_delay).release(element).perform()

@keyword
def open_context_menu(self, locator: Union[WebElement, str]):
"""Opens the context menu on the element identified by ``locator``."""
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.context_click(element).perform()

@keyword
Expand Down Expand Up @@ -954,12 +954,12 @@ def press_keys(self, locator: Union[WebElement, None, str] = None, *keys: str):
element = self.find_element(locator)
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
ActionChains(self.driver).click(element).perform()
ActionChains(self.driver, duration=self.ctx.action_chain_delay).click(element).perform()
else:
self.info(f"Sending key(s) {keys} to page.")
element = None
for parsed_key in parsed_keys:
actions = ActionChains(self.driver)
actions = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
for key in parsed_key:
if key.special:
self._press_keys_special_keys(actions, element, parsed_key, key)
Expand Down Expand Up @@ -1009,7 +1009,7 @@ def mouse_down_on_link(self, locator: Union[WebElement, str]):
element = self.find_element(locator, tag="link")
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.click_and_hold(element).perform()

@keyword
Expand Down Expand Up @@ -1059,7 +1059,7 @@ def mouse_down_on_image(self, locator: Union[WebElement, str]):
element = self.find_element(locator, tag="image")
# _unwrap_eventfiring_element can be removed when minimum required Selenium is 4.0 or greater.
element = _unwrap_eventfiring_element(element)
action = ActionChains(self.driver)
action = ActionChains(self.driver, duration=self.ctx.action_chain_delay)
action.click_and_hold(element).perform()

@keyword
Expand Down
1 change: 1 addition & 0 deletions src/SeleniumLibrary/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
is_truthy,
WINDOWS,
_convert_timeout,
_convert_delay,
) # noqa


Expand Down
7 changes: 7 additions & 0 deletions src/SeleniumLibrary/utils/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@
def is_noney(item):
return item is None or is_string(item) and item.upper() == "NONE"

def _convert_delay(delay):
if isinstance(delay, timedelta):
return delay.microseconds // 1000
else:
x = timestr_to_secs(delay)
return int( x * 1000)


def _convert_timeout(timeout):
if isinstance(timeout, timedelta):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ SeleniumLibrary can be imported with several optional arguments.
Default value for `timeouts` used with ``Wait ...`` keywords.
- ``implicit_wait``:
Default value for `implicit wait` used when locating elements.
- ``action_chain_delay``:
Default value for `ActionChains` delay to wait in between actions.
- ``run_on_failure``:
Default action for the `run-on-failure functionality`.
- ``screenshot_root_directory``:
Expand Down
2 changes: 1 addition & 1 deletion utest/test/api/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()), 173)
self.assertEqual(len(sl.get_keyword_names()), 175)

def test_parse_library(self):
plugin = "path.to.MyLibrary"
Expand Down
19 changes: 19 additions & 0 deletions utest/test/keywords/test_browsermanagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ def test_set_selenium_timeout_only_affects_open_browsers():
verifyNoMoreInteractions(second_browser)


def test_action_chain_delay_default():
sl = SeleniumLibrary()
assert sl.action_chain_delay == 250, f"Delay should have 250"


def test_set_action_chain_delay_default():
sl = SeleniumLibrary()
sl.set_action_chain_delay("3.0")
assert sl.action_chain_delay == 3000, f"Delay should have 3000"

sl.set_action_chain_delay("258 milliseconds")
assert sl.action_chain_delay == 258, f"Delay should have 258"


def test_get_action_chain_delay_default():
sl = SeleniumLibrary()
sl.set_action_chain_delay("300 milliseconds")
assert sl.get_action_chain_delay() == 0.3

def test_selenium_implicit_wait_default():
sl = SeleniumLibrary()
assert sl.implicit_wait == 0.0, "Wait should have 0.0"
Expand Down
22 changes: 20 additions & 2 deletions utest/test/keywords/test_keyword_arguments_element.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import pytest
from mockito import mock, unstub, when

from mockito import mock, unstub, when, matchers
from SeleniumLibrary.keywords import ElementKeywords
import SeleniumLibrary.keywords.element as SUT


@pytest.fixture(scope="function")
def element():
ctx = mock()
ctx._browser = mock()
ctx.action_chain_delay = 251
return ElementKeywords(ctx)


Expand All @@ -27,3 +28,20 @@ def test_element_text_should_be(element):
with pytest.raises(AssertionError) as error:
element.element_text_should_be(locator, "not text", "foobar")
assert "foobar" in str(error.value)



def test_action_chain_delay_in_elements(element):
locator = "//div"
webelement = mock()
when(element).find_element(locator).thenReturn(webelement)

chain_mock = mock()
expected_delay_in_ms = 1000
element.ctx.action_chain_delay = expected_delay_in_ms
when(chain_mock).move_to_element(matchers.ANY).thenReturn(mock())
when(SUT).ActionChains(matchers.ANY, duration=expected_delay_in_ms).thenReturn(chain_mock)
element.scroll_element_into_view(locator)