Skip to content
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
12 changes: 12 additions & 0 deletions doc/control.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ from paf.control import change
with change(retry_count=3, wait_after_fail=0):
retry(lambda: text_element.expect.count.be(1), lambda e: text_element.webdriver.refresh())
```

## Execution speed (experimental)

You can change the execution speed of several actions like:

```python
from paf.control import change, ExecutionSpeed

with change(execution_speed=ExecutionSpeed.fast):
element.type("Hello World")
element.click()
```
92 changes: 52 additions & 40 deletions paf/uielement.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from selenium.common import NoSuchShadowRootException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By as SeleniumBy
from selenium.webdriver.remote.shadowroot import ShadowRoot
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.color import Color
Expand Down Expand Up @@ -306,6 +307,8 @@ def __init__(
self._ui_element = ui_element
self.__by = by
self.__index = index
self.__web_element_id = None
self.__has_shadow_root = None
super().__init__(name, parent)

@property
Expand Down Expand Up @@ -341,6 +344,21 @@ def _filter_web_elements(self, web_elements: List[WebElement]):
else:
return web_elements

def __detect_shadow_root(self, ui_element: "DefaultUiElement", web_element: WebElement) -> WebElement|ShadowRoot:
if ui_element.__web_element_id != web_element.id:
ui_element.__web_element_id = web_element.id
try:
web_element_context = web_element.shadow_root
ui_element.__has_shadow_root = True
return web_element_context
except NoSuchShadowRootException as e:
ui_element.__has_shadow_root = False
return web_element
elif ui_element.__has_shadow_root:
return web_element.shadow_root
else:
return web_element

@contextmanager
def _find_web_elements(self) -> ContextManager[List[WebElement]]:
if self._ui_element:
Expand All @@ -350,13 +368,8 @@ def _find_web_elements(self) -> ContextManager[List[WebElement]]:
self._webdriver.switch_to.frame(web_element)
web_elements = self._webdriver.find_elements(self._by.by, self._by.value)
else:
web_element_context = web_element
try:
web_element_context = web_element.shadow_root
except NoSuchShadowRootException as e:
pass

web_elements = web_element_context.find_elements(self._by.by, self.__relative_selector(self._by))
web_element = self.__detect_shadow_root(self._ui_element, web_element)
web_elements = web_element.find_elements(self._by.by, self.__relative_selector(self._by))

yield self._filter_web_elements(web_elements)

Expand All @@ -367,19 +380,7 @@ def _find_web_elements(self) -> ContextManager[List[WebElement]]:
else:
raise Exception(f"{self.name_path} initialized without WebDriver nor UiElement")

# @contextmanager
# def find_web_element(self) -> ContextManager[WebElement]:
# pass
# with self._find_web_elements() as web_elements:
# count = len(web_elements)
# if self._by.is_unique and count != 1:
# raise NotUniqueException()
# elif count > self._index:
# yield web_elements[self._index]
# else:
# raise NotFoundException()

def _web_element_action_sequence(self, action: Consumer[WebElement], action_name: str):
def __web_element_action_sequence(self, action: Consumer[WebElement], action_name: str):
action_listener = inject.instance(ActionListener)

def _sequence():
Expand All @@ -394,10 +395,6 @@ def _sequence():
action_listener.action_failed_finally(action_name, self, exception)
raise exception

def click(self):
self._web_element_action_sequence(lambda x: x.click(), "click")
return self

def take_screenshot(self, file_name: str = None) -> Path | None:
with self.find_web_element() as web_element:
dir = Path(Property.env(Property.PAF_SCREENSHOTS_DIR))
Expand Down Expand Up @@ -426,7 +423,22 @@ def send_keys(self, value: str):
def _action(web_element: WebElement):
self.__send_keys(web_element, value)

self._web_element_action_sequence(_action, "send_keys")
self.__web_element_action_sequence(_action, "send_keys")
return self

def __create_action_chain(self):
actions = ActionChains(self._webdriver)
config = get_config()
if config.execution_speed:
actions = actions.pause(config.execution_speed.get_random())
return actions

def click(self):
def _action(web_element: WebElement):
actions = self.__create_action_chain()
actions.click(web_element).perform()

self.__web_element_action_sequence(_action, "click")
return self

def type(self, value: str):
Expand All @@ -435,70 +447,70 @@ def _action(web_element: WebElement):
self.__send_keys(web_element, value)
assert web_element.get_attribute("value") == value

self._web_element_action_sequence(_action, "type")
self.__web_element_action_sequence(_action, "type")
return self

def hover(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions = self.__create_action_chain()
actions.move_to_element(web_element).perform()

self._web_element_action_sequence(_action, "hover")
self.__web_element_action_sequence(_action, "hover")

def context_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions = self.__create_action_chain()
actions.context_click(web_element).perform()

self._web_element_action_sequence(_action, "context_click")
self.__web_element_action_sequence(_action, "context_click")

def long_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions = self.__create_action_chain()
actions.click_and_hold(web_element).perform()

self._web_element_action_sequence(_action, "long_click")
self.__web_element_action_sequence(_action, "long_click")

def double_click(self):
def _action(web_element: WebElement):
actions = ActionChains(self._webdriver)
actions = self.__create_action_chain()
actions.double_click(web_element).perform()

self._web_element_action_sequence(_action, "double_click")
self.__web_element_action_sequence(_action, "double_click")

def drag_and_drop_to(self, target_ui_element: "UiElement"):
def _action(web_element: WebElement):
with target_ui_element.find_web_element() as target:
actions = ActionChains(self._webdriver)
actions.drag_and_drop(web_element, target).perform()

self._web_element_action_sequence(_action, "drag_and_drop_to")
self.__web_element_action_sequence(_action, "drag_and_drop_to")

def clear(self):
self._web_element_action_sequence(lambda x: x.clear(), "clear")
self.__web_element_action_sequence(lambda x: x.clear(), "clear")
return self

def submit(self):
self._web_element_action_sequence(lambda x: x.submit(), "submit")
self.__web_element_action_sequence(lambda x: x.submit(), "submit")
return self

def scroll_into_view(self, x: int = 0, y: int = 0):
def _action(web_element: WebElement):
script.scroll_to_center(self._webdriver, web_element, Point(x, y))

self._web_element_action_sequence(_action, "scroll_into_view")
self.__web_element_action_sequence(_action, "scroll_into_view")

def scroll_to_top(self, x: int = 0, y: int = 0):
def _action(web_element: WebElement):
script.scroll_to_top(self._webdriver, web_element, Point(x, y))

self._web_element_action_sequence(_action, "scroll_to_top")
self.__web_element_action_sequence(_action, "scroll_to_top")

def highlight(self, color: Color = Color.from_string("#0f0"), seconds: float = 2):
def _action(web_element: WebElement):
script.highlight(self._webdriver, web_element, color, math.floor(seconds * 1000))

self._web_element_action_sequence(_action, "highlight")
self.__web_element_action_sequence(_action, "highlight")

def __iter__(self):
for i in range(self._count_elements()):
Expand Down
15 changes: 15 additions & 0 deletions test/test_uielement.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,5 +350,20 @@ def test_inexistent_ui_element_wrapper():
assert False


def test_web_element_keeps_unchanged(finder: FinderPage):
finder.open("https://testpages.herokuapp.com/styled/basic-web-page-test.html")
p = finder.find("#para1")
with p.find_web_element() as web_element:
id_before = web_element.id

with p.find_web_element() as web_element:
assert web_element.id == id_before

def test_shadow_root_access(finder: FinderPage):
finder.open("https://practice.expandtesting.com/shadowdom")
shadow_host = finder.find("#shadow-host")
my_btn = shadow_host.find("#my-btn")
my_btn.expect.text.be("This button is inside a Shadow DOM.")

def teardown_module():
inject.instance(WebDriverManager).shutdown_all()