#!/usr/bin/python3
#
# Selenium Tests
#
# SPDX-FileCopyrightText: 2017-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
import json
import logging
import time
from typing import Any
import selenium.common.exceptions as selenium_exceptions
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from univention.testing.selenium.utils import expand_path
logger = logging.getLogger(__name__)
[docs]
class Interactions:
[docs]
def click_text(self, text: str, **kwargs: Any) -> None:
logger.info("Clicking the text %r", text)
self.click_element(f'//*[contains(text(), "{text}")]', **kwargs)
[docs]
def click_checkbox_of_grid_entry(self, name: str, **kwargs: Any) -> None:
logger.info("Clicking the checkbox of the grid entry %r", name)
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " dgrid-cell ")][@role="gridcell"]/descendant-or-self::node()[contains(text(), "%s")]/../..//input[@type="checkbox"]/..'
% (name,),
**kwargs,
)
[docs]
def click_checkbox_of_dojox_grid_entry(self, name: str, **kwargs: Any) -> None:
logger.info("Clicking the checkbox of the dojox grid entry %r", name)
self.click_element(
expand_path('//*[@containsClass="dojoxGridCell"][@role="gridcell"][contains(text(), "%s")]/preceding-sibling::*[1]')
% (name,),
**kwargs,
)
[docs]
def click_grid_entry(self, name: str, **kwargs: Any) -> None:
logger.info("Clicking the grid entry %r", name)
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " dgrid-cell ")][@role="gridcell"]/descendant-or-self::node()[contains(text(), "%s")]'
% (name,),
**kwargs,
)
[docs]
def click_tree_entry(self, name: str, **kwargs: Any) -> None:
logger.info("Clicking the tree entry %r", name)
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " dgrid-column-label ")][contains(text(), "%s")]'
% (name,),
**kwargs,
)
[docs]
def click_tile(self, tilename: str, **kwargs: Any) -> None:
logger.info("Clicking the tile %r", tilename)
try:
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " umcGalleryName ")][text() = "%s"]'
% (tilename,),
**kwargs,
)
except selenium_exceptions.TimeoutException:
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " umcGalleryName ")][@title = "%s"]'
% (tilename,),
**kwargs,
)
[docs]
def click_tab(self, tabname: str, **kwargs: Any) -> None:
logger.info("Clicking the tab %r", tabname)
self.click_element(
'//*[contains(concat(" ", normalize-space(@class), " "), " tabLabel ")][text() = "%s"]'
% (tabname,),
**kwargs,
)
[docs]
def click_element(self, xpath: str, scroll_into_view: bool = False, timeout: float = 60, right_click: bool = False) -> None:
"""
Click on the element which is found by the given xpath.
Only use with caution when there are multiple elements with that xpath.
Waits for the element to be clickable before attempting to click.
"""
elems = webdriver.support.ui.WebDriverWait(xpath, timeout).until(
self.get_all_enabled_elements, f'click_element({xpath!r}, scroll_into_view={scroll_into_view!r}, timeout={timeout!r}, right_click={right_click!r})',
)
if len(elems) != 1:
logger.warning("Found %d clickable elements instead of 1. Trying to click on the first one.", len(elems))
if scroll_into_view:
self.driver.execute_script("arguments[0].scrollIntoView();", elems[0])
limit = timeout
if right_click:
# context_click will always work since it triggers the browser context menu
# instead of throwing ElementClickInterceptedException.
# So we do not need to put it in the loop below
ActionChains(self.driver).context_click(elems[0]).perform()
else:
while True:
try:
elems[0].click()
break
except selenium_exceptions.ElementClickInterceptedException:
limit -= 1
if limit == 0:
raise
time.sleep(1)
[docs]
def get_all_enabled_elements(self, xpath: str) -> list[Any]:
elems = self.driver.find_elements(By.XPATH, xpath)
try:
return [
elem
for elem in elems
if elem.is_enabled() and elem.is_displayed()
]
except selenium_exceptions.StaleElementReferenceException:
pass
return []
[docs]
def upload_image(self, img_path: str, button_label: str = 'Upload', timeout: int = 60, xpath_prefix: str = '') -> None:
"""
Get an ImageUploader widget on screen and upload the given img_path.
Which ImageUploader widget is found can be isolated by specifying 'xpath_prefix'
which would be an xpath pointing to a specific container/section etc.
"""
uploader_button_xpath = f'//*[contains(@id, "_ImageUploader_")]//*[text()="{button_label}"]'
self.wait_until_element_visible(xpath_prefix + uploader_button_xpath)
uploader_xpath = '//*[contains(@id, "_ImageUploader_")]//input[@type="file"]'
logger.info("Getting the uploader with xpath: %s%s", xpath_prefix, uploader_xpath)
uploader = self.driver.find_element(By.XPATH, xpath_prefix + uploader_xpath)
logger.info("Uploading the image: %s", img_path)
uploader.send_keys(img_path)
logger.info("Waiting for upload to finish")
time.sleep(1) # wait_for_text('Uploading...') is too inconsistent
self.wait_until_element_visible(xpath_prefix + uploader_button_xpath)
[docs]
def drag_and_drop(self, source: Any | str, target: Any | str, find_by: str = 'xpath') -> None:
"""Wrapper for selenium.webdriver.common.action_chains.drag_and_drop"""
by = {'xpath': By.XPATH, 'id': By.ID}[find_by]
if isinstance(source, str):
source = self.driver.find_element(by, source)
if isinstance(target, str):
target = self.driver.find_element(by, target)
ActionChains(self.driver).drag_and_drop(source, target).perform()
[docs]
def drag_and_drop_by_offset(self, source: Any | str, xoffset: int, yoffset: int, find_by: str = 'xpath') -> None:
"""Wrapper for selenium.webdriver.common.action_chains.drag_and_drop_by_offset"""
by = {'xpath': By.XPATH, 'id': By.ID}[find_by]
if isinstance(source, str):
source = self.driver.find_element(by, source)
ActionChains(self.driver).drag_and_drop_by_offset(source, xoffset, yoffset).perform()