HomeDashboard/.venv/lib/python3.12/site-packages/nicegui/testing/user_interaction.py
2026-01-03 14:54:18 +01:00

150 lines
6.6 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from typing_extensions import Self
from nicegui import background_tasks, events, ui
from nicegui.element import Element
from nicegui.elements.mixins.disableable_element import DisableableElement
from nicegui.elements.mixins.value_element import ValueElement
if TYPE_CHECKING:
from .user import User
T = TypeVar('T', bound=Element)
class UserInteraction(Generic[T]):
def __init__(self, user: User, elements: set[T], target: str | type[T] | None) -> None:
"""Interaction object of the simulated user.
This will be returned by the ``find`` method of the ``user`` fixture in pytests.
It can be used to perform interaction with the found elements.
"""
self.user = user
for element in elements:
assert isinstance(element, ui.element)
self.elements = elements
self.target = target
def trigger(self, event: str, args: Any = None) -> Self:
"""Trigger the given event on the elements selected by the simulated user.
:param event: the event type to trigger (e.g. "keydown.enter", "click", ...)
:param args: optional event arguments to pass to the handler (default: ``None``)
"""
if args is None:
args = {}
assert self.user.client
with self.user.client:
for element in self.elements:
if isinstance(element, ui.input) and event == 'keydown.tab':
autocomplete: list[str] = element.props['_autocomplete']
for option in autocomplete:
if option.startswith(element.value):
element.value = option
break
for listener in element._event_listeners.values(): # pylint: disable=protected-access
if listener.type != event:
continue
event_arguments = events.GenericEventArguments(sender=element, client=self.user.client, args=args)
events.handle_event(listener.handler, event_arguments)
return self
def type(self, text: str) -> Self:
"""Type the given text into the selected elements.
Note: All elements must have a ``value`` attribute.
"""
assert self.user.client
with self.user.client:
for element in self.elements:
if isinstance(element, DisableableElement) and not element.enabled:
continue
assert isinstance(element, (ui.input, ui.editor, ui.codemirror))
element.value = (element.value or '') + text
return self
def click(self) -> Self:
"""Click the selected elements."""
assert self.user.client
with self.user.client:
for element in self.elements:
if isinstance(element, DisableableElement) and not element.enabled:
continue
if isinstance(element, ui.link):
href = element.props.get('href', '#')
background_tasks.create(self.user.open(href), name=f'open {href}')
return self
if isinstance(element, ui.select):
if element.is_showing_popup:
if isinstance(element.options, dict):
target_value = next((k for k, v in element.options.items() if v == self.target), '')
else:
target_value = self.target
if element.multiple:
if target_value in element.value:
element.value = [v for v in element.value if v != target_value]
elif target_value in element._values: # pylint: disable=protected-access
element.value = [*element.value, target_value]
else:
element._is_showing_popup = False # pylint: disable=protected-access
else:
element.value = target_value
element._is_showing_popup = False # pylint: disable=protected-access
else:
element._is_showing_popup = True # pylint: disable=protected-access
return self
elif isinstance(element, ui.radio):
if isinstance(element.options, dict):
target_value = next((k for k, v in element.options.items() if v == self.target), '')
else:
target_value = self.target
element.value = target_value
return self
elif isinstance(element, ui.tree) and isinstance(self.target, str):
NODE_KEY = element.props.get('node-key')
LABEL_KEY = element.props.get('label-key')
target_key = next((
node[NODE_KEY]
for node in element.nodes(visible=True)
if self.target == node.get(LABEL_KEY)
), None)
if target_key is None:
return self
expanded_set = set(element.props.get('expanded', [node[NODE_KEY] for node in element.nodes()]))
if target_key in expanded_set:
expanded_set.remove(target_key)
else:
expanded_set.add(target_key)
element.props['expanded'] = list(expanded_set)
element.update()
return self
for listener in element._event_listeners.values(): # pylint: disable=protected-access
if listener.element_id != element.id:
continue
args = not element.value if isinstance(element, (ui.checkbox, ui.switch)) else None
event_arguments = events.GenericEventArguments(sender=element, client=self.user.client, args=args)
events.handle_event(listener.handler, event_arguments)
return self
def clear(self) -> Self:
"""Clear the selected elements.
Note: All elements must have a ``value`` attribute.
"""
assert self.user.client
with self.user.client:
for element in self.elements:
if isinstance(element, DisableableElement) and not element.enabled:
continue
assert isinstance(element, ValueElement)
element.value = None
return self