Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/zzgt6xwydtfz7r70untitledPlain Text
import re
import time
from typing import Self
import allure
from playwright.sync_api import Locator, Page, expect
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
from framework.components.base.button import Button
from framework.components.base.confirmation_dialog import ConfirmationDialog
from framework.components.base.domain_table import DomainTable
from framework.components.base.dropdown import Dropdown
from framework.components.base.filter_table import KALKI_TABLE_CONTROLS
from framework.components.base.item_group import ItemGroup
from framework.components.base.saved_filters_menu import SavedFiltersMenu
from framework.components.base.table_cell import (
ActionsCell,
BadgeCell,
CheckboxCell,
CheckboxHeaderCell,
TableDataCell,
)
from framework.components.base.table_row import SelectableTableDataRow
from framework.components.custom.user_management.add_user_drawer import AddUserDrawer
from framework.components.custom.user_management.filter_users_drawer import (
FilterUsersDrawer,
)
from framework.components.custom.user_management.user_details_drawer import (
UserDetailsDrawer,
)
from framework.core.constants import AriaRoles
from framework.core.environment import Environment
from framework.core.locator_strategy import LocatorStrategy
from framework.core.locators import Locators
from framework.utils import wait_helper
# "Select" has no header text of its own - its header cell renders only a
# checkbox - so it's resolved structurally via get_cell_by_checkbox_header()
# rather than by name, and is left out of the name-lookup columns below.
class UserRow(SelectableTableDataRow):
"""A user-management row with named access to its domain columns."""
def get_select_cell(self) -> CheckboxCell:
cell = self.get_cell("__checkbox__")
return CheckboxCell(
self.get_page(),
LocatorStrategy(selector=":scope"),
target=cell.get_locator(),
)
def get_username_cell(self) -> TableDataCell:
return self.get_cell_by_column_name("Username")
def get_name_cell(self) -> TableDataCell:
return self.get_cell_by_column_name("Name")
def get_email_cell(self) -> TableDataCell:
return self.get_cell_by_column_name("Email")
def get_identity_cell(self) -> BadgeCell:
cell = self.get_cell_by_column_name("Identity")
return BadgeCell.from_locator(self.get_page(), cell.get_locator())
def get_default_cell(self) -> TableDataCell:
return self.get_cell_by_column_name("Default")
def get_status_cell(self) -> BadgeCell:
cell = self.get_cell_by_column_name("Status")
return BadgeCell.from_locator(self.get_page(), cell.get_locator())
def is_disabled(self, timeout: int = Environment.TIMEOUT_MS) -> bool:
return self.get_status_cell().get_badge_text().strip().casefold() == "disabled"
def is_enabled(self, timeout: int = Environment.TIMEOUT_MS) -> bool:
return self.get_status_cell().get_badge_text().strip().casefold() == "active"
def get_actions_cell(self) -> ActionsCell:
cell = self.get_cell_by_column_name("Actions")
return ActionsCell(
self.get_page(),
LocatorStrategy(selector=":scope"),
target=cell.get_locator(),
)
class UsersTable(DomainTable[UserRow]):
"""User-management table behavior built from generic table components."""
def __init__(self, page: Page) -> None:
super().__init__(
page,
LocatorStrategy(role=AriaRoles.MAIN),
row_type=UserRow,
name_column="Username",
name_exact=False,
controls=KALKI_TABLE_CONTROLS,
is_row_selectable=True,
)
self.add_user_button = Button.from_role(
page,
AriaRoles.BUTTON,
"Add User",
target=self.get_application_scope(),
exact=True,
)
def click_add_user(self, timeout: int = Environment.TIMEOUT_MS) -> Self:
self.add_user_button.click(timeout=timeout)
return self
@allure.step("Opening add user option for user")
def open_add_user(self, timeout: int = Environment.TIMEOUT_MS) -> AddUserDrawer:
self.click_add_user(timeout=timeout)
drawer = AddUserDrawer(self.get_page())
drawer.wait_until_open(timeout=timeout)
return drawer
@allure.step("Opening filters for the users table")
def open_filters(self, timeout: int = Environment.TIMEOUT_MS) -> FilterUsersDrawer:
"""Open and return the filter drawer for the users table."""
Button.from_role(
self.get_page(),
AriaRoles.BUTTON,
"Filters",
target=self.get_application_scope(),
exact=True,
).click(timeout=timeout)
drawer = FilterUsersDrawer(self.get_page())
drawer.wait_until_open(timeout=timeout)
return drawer
def get_active_filter_count(self, timeout: int = Environment.TIMEOUT_MS) -> int:
"""Return the number shown on the Filters button badge, or 0 when
the button has no badge (no filters applied)."""
button = Button.from_role(
self.get_page(),
AriaRoles.BUTTON,
re.compile(r"^Filters"),
target=self.get_application_scope(),
)
match = re.search(r"\d+", button.get_text())
return int(match.group()) if match else 0
def get_saved_filter_count(self, timeout: int = Environment.TIMEOUT_MS) -> int:
"""Return the number shown on the Saved Filters button badge, or 0
when no saved-filter count is displayed."""
button = Button.from_role(
self.get_page(),
AriaRoles.BUTTON,
"Saved Filters",
target=self.get_application_scope(),
exact=True,
)
match = re.search(r"\((\d+)\)", button.get_text())
return int(match.group(1)) if match else 0
def open_user_details(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> UserDetailsDrawer:
"""Open the detail drawer for the user row containing ``text``."""
user = self.get_user(text)
drawer_name = user.get_username_cell().get_text().strip().split("\n", 1)[-1]
user.get_locator().click(timeout=timeout)
drawer = UserDetailsDrawer(self.get_page(), name=drawer_name)
drawer.wait_until_open(timeout=timeout)
return drawer
def create_user(
self,
username: str,
first_name: str,
email: str,
identity_name: str,
/,
timeout: int = Environment.TIMEOUT_MS,
) -> Self:
self.open_add_user(timeout=timeout).create(
username,
first_name,
email,
identity_name,
timeout=timeout,
)
self.expect_user(username, timeout=timeout)
return self
def get_locator(self) -> Locator:
return self.table.get_locator()
def get_select_all_cell(self) -> CheckboxHeaderCell:
header_cell = self.table.get_heading_cell_by_name("__checkbox__")
return CheckboxHeaderCell(
self.get_page(),
LocatorStrategy(selector=":scope"),
target=header_cell.get_locator(),
)
def select_all(self, timeout: int = Environment.TIMEOUT_MS) -> Self:
self.get_select_all_cell().check(timeout=timeout)
return self
def clear_selection(self, timeout: int = Environment.TIMEOUT_MS) -> Self:
self.get_select_all_cell().uncheck(timeout=timeout)
return self
def select_user(self, text: str, /, timeout: int = Environment.TIMEOUT_MS) -> Self:
self.get_user(text).get_select_cell().check(timeout=timeout)
return self
def unselect_user(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> Self:
self.get_user(text).get_select_cell().uncheck(timeout=timeout)
return self
def get_user(self, text: str, /) -> UserRow:
return self.get_named_row(text)
def has_user(
self,
text: str,
/,
timeout: int = Environment.TIMEOUT_MS,
) -> bool:
try:
return self.has_named_row(text, timeout=timeout)
except (AssertionError, ValueError):
return False
def expect_user(
self, text: str, /, *, timeout: int = Environment.TIMEOUT_MS
) -> Self:
self.expect_named_row(text, timeout=timeout)
return self
def expect_no_user(
self, text: str, /, *, timeout: int = Environment.TIMEOUT_MS
) -> Self:
expect(
Locators.get_by_role(self.get_locator(), AriaRoles.ROW, has_text=text)
).to_have_count(0, timeout=timeout)
return self
def get_actions_dropdown_by_user(self, text: str, /) -> Dropdown:
"""Return the actions dropdown for the user row containing `text`."""
row = self.get_user(text)
return row.get_actions_cell().get_dropdown()
def open_actions_menu_by_user(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> ItemGroup:
"""Open and return the actions menu for the matching user row."""
return self.open_row_actions(text, timeout=timeout)
def click_action_by_user(
self,
text: str,
action_name: str,
/,
timeout: int = Environment.TIMEOUT_MS,
) -> Self:
"""Click an action in the matching user's actions menu."""
self.click_row_action(text, action_name, timeout=timeout)
return self
def disable_user(self, text: str, /, timeout: int = Environment.TIMEOUT_MS) -> Self:
"""Disable the user row containing `text`."""
self.open_disable_user(text, timeout=timeout).confirm(timeout=timeout)
# self._wait_for_user_status(text, "disabled", timeout=timeout)
return self
def open_disable_user(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> ConfirmationDialog:
"""Open the disable confirmation dialog for the matching user row."""
user = self.get_user(text)
if not user.is_enabled(timeout=timeout):
raise ValueError(f"User '{text}' is not enabled and cannot be disabled.")
self.click_action_by_user(text, "Disable", timeout=timeout)
confirmation = ConfirmationDialog(
self.get_page(),
strategy=LocatorStrategy(
role=AriaRoles.DIALOG,
role_name=re.compile(r"^Disable User"),
),
title_strategy=LocatorStrategy(
role=AriaRoles.HEADING,
role_name=re.compile(r"^Disable User"),
),
confirm_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name=self.t("buttons.disable"),
exact=True,
),
cancel_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name=self.t("buttons.cancel"),
exact=True,
),
)
confirmation.wait_until_open(timeout=timeout)
return confirmation
def enable_user(self, text: str, /, timeout: int = Environment.TIMEOUT_MS) -> Self:
"""Enable a disabled user row."""
self.open_enable_user(text, timeout=timeout).confirm(timeout=timeout)
self._wait_for_user_status(text, "active", timeout=timeout)
return self
def _wait_for_user_status(
self,
text: str,
expected_status: str,
/,
timeout: int,
) -> Self:
deadline = time.monotonic() + timeout / 1000
while time.monotonic() < deadline:
try:
status = self.get_user(text).get_status_cell().get_badge_text()
except PlaywrightTimeoutError:
status = ""
if status.strip().casefold() == expected_status:
return self
self.get_page().wait_for_timeout(100)
raise AssertionError(
f"User '{text}' did not reach status '{expected_status}' "
f"within {timeout} ms."
)
return self
def open_enable_user(
self, username: str, /, timeout: int = Environment.TIMEOUT_MS
) -> ConfirmationDialog:
"""Open the enable confirmation for a user whose status is disabled."""
user = self.get_user(username)
if not user.is_disabled(timeout=timeout):
raise ValueError(
f"User '{username}' is not disabled and cannot be enabled."
)
self.click_action_by_user(username, "Enable", timeout=timeout)
confirmation = ConfirmationDialog(
self.get_page(),
title_strategy=LocatorStrategy(
role=AriaRoles.HEADING,
role_name=re.compile(r"^Enable User"),
),
strategy=LocatorStrategy(
role=AriaRoles.DIALOG,
role_name=re.compile(r"^Enable User"),
),
confirm_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name="Enable",
exact=True,
),
cancel_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name=self.t("buttons.cancel"),
exact=True,
),
)
confirmation.wait_until_open(timeout=timeout)
return confirmation
def delete_user(self, text: str, /, timeout: int = Environment.TIMEOUT_MS) -> Self:
"""Delete the user row containing `text`."""
if not self.has_user(text, timeout=timeout):
raise ValueError(f"User '{text}' was not found.")
self.open_delete_user(text, timeout=timeout).confirm(timeout=timeout)
# self.expect_no_user(text, timeout=timeout)
return self
def set_as_default(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> Self:
user = self.get_user(text)
# if user.get_default_cell().get_text().strip().casefold() == "default":
# return self
self.click_action_by_user(text, "Set as Default", timeout=timeout)
wait_helper.wait_for_toast(
self.get_application_scope(),
timeout_ms=timeout,
)
return self
def open_delete_user(
self, text: str, /, timeout: int = Environment.TIMEOUT_MS
) -> ConfirmationDialog:
"""Open the delete confirmation dialog for the matching user row."""
self.click_action_by_user(text, "Delete", timeout=timeout)
confirmation = ConfirmationDialog(
self.get_page(),
title_strategy=LocatorStrategy(
role=AriaRoles.HEADING,
role_name=re.compile(r"^Delete User"),
),
strategy=LocatorStrategy(
role=AriaRoles.DIALOG,
role_name=re.compile(r"^Delete User"),
),
confirm_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name=self.t("buttons.delete"),
exact=True,
),
cancel_button_strategy=LocatorStrategy(
role=AriaRoles.BUTTON,
role_name=self.t("buttons.cancel"),
exact=True,
),
)
confirmation.wait_until_open(timeout=timeout)
return confirmation
def send_activation_mail(
self,
text: str,
/,
timeout: int = Environment.TIMEOUT_MS,
) -> Self:
"""Send an activation email to the specified user."""
self.click_action_by_user(
text,
"Send Activation Mail",
timeout=timeout,
)
wait_helper.wait_for_toast(
self.get_application_scope(),
expected_text="Activation mail sent",
timeout_ms=timeout,
)
return self
###############
def _saved_filters_button(self) -> Button:
return Button.from_role(
self.get_page(),
AriaRoles.BUTTON,
"Saved Filters",
target=self.get_application_scope(),
exact=True,
)
@allure.step("Opening the saved filters dropdown")
def open_saved_filters(
self, timeout: int = Environment.TIMEOUT_MS
) -> SavedFiltersMenu:
"""Open the Saved Filters dropdown (if closed) and return it."""
button = self._saved_filters_button()
if not Locators.is_aria_expanded(button.get_locator(), timeout=timeout):
button.click(timeout=timeout)
return SavedFiltersMenu(
self.get_page(),
LocatorStrategy(
role=AriaRoles.LISTBOX, role_name="Saved Filters", exact=True
),
target=self.get_application_scope(),
)
def close_saved_filters(self, timeout: int = Environment.TIMEOUT_MS) -> Self:
"""Close the Saved Filters dropdown if it is open."""
button = self._saved_filters_button()
if Locators.is_aria_expanded(button.get_locator(), timeout=timeout):
button.click(timeout=timeout)
return self
def has_saved_filters(self, timeout: int = Environment.TIMEOUT_MS) -> bool:
"""Return True if at least one saved filter exists ("No saved
filters yet" is shown otherwise)."""
menu = self.open_saved_filters(timeout=timeout)
try:
return menu.has_saved_filters()
finally:
self.close_saved_filters(timeout=timeout)
def get_saved_filter_names(
self, timeout: int = Environment.TIMEOUT_MS
) -> tuple[str, ...]:
"""Return the names of every saved filter, in order."""
menu = self.open_saved_filters(timeout=timeout)
try:
return tuple(option.get_name() for option in menu.get_options())
finally:
self.close_saved_filters(timeout=timeout)
def has_saved_filter(
self, name: str, /, timeout: int = Environment.TIMEOUT_MS
) -> bool:
"""Return True if a saved filter with exactly this name is listed."""
return name in self.get_saved_filter_names(timeout=timeout)
@allure.step("Deleting saved filter '{name}'")
def delete_saved_filter(
self, name: str, /, timeout: int = Environment.TIMEOUT_MS
) -> Self:
"""Delete the saved filter with this name."""
menu = self.open_saved_filters(timeout=timeout)
try:
menu.delete(name, timeout=timeout)
finally:
self.close_saved_filters(timeout=timeout)
return self
18,794 chars · codepastes.com