users_table

Pasted Sep 30, 2026, 6:11 PM1 file536 lines

Scan to open on your phone

Point a camera at the QR code - it opens this exact paste, no app needed.

https://codepastes.com/zzgt6xwydtfz7r70
untitled
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