diff --git a/src/vip_tests/workbench/conftest.py b/src/vip_tests/workbench/conftest.py index 34764734..48d50577 100644 --- a/src/vip_tests/workbench/conftest.py +++ b/src/vip_tests/workbench/conftest.py @@ -92,6 +92,42 @@ def pytest_collection_modifyitems( # instead of timing out on an opaque "Locator expected to be visible" error. TERMINAL_SESSION_FAILURE_STATES = ("Failed",) +# --------------------------------------------------------------------------- +# Resource profile helpers +# Shared between test_session_capacity.py and test_session_capacity_k8s.py, +# both of which need to detect and skip resource profiles that Workbench +# renders as visible-but-disabled for the authenticated user. +# --------------------------------------------------------------------------- + + +class ResourceProfileDisabled(Exception): + """Raised when the target resource profile is present but disabled for the user. + + Workbench renders resource profiles the authenticated user is not entitled + to (e.g. a group-restricted profile) as visible options with + ``aria-disabled='true'`` / ``data-disabled``. Clicking one just blocks + until Playwright's timeout, so ``_launch_session`` raises this instead and + lets the caller record the profile as unavailable and move on. + """ + + def __init__(self, profile: str) -> None: + super().__init__(profile) + self.profile = profile + + +def _option_is_disabled(option: Locator) -> bool: + """Return True if a ``[role='option']`` is disabled for the current user. + + Radix-based selects mark unavailable options with ``aria-disabled='true'`` + and an (empty-valued) ``data-disabled`` attribute; ``get_attribute`` + returns ``""`` for the latter, so test for presence rather than truthiness. + """ + return ( + option.get_attribute("aria-disabled") == "true" + or option.get_attribute("data-disabled") is not None + ) + + # --------------------------------------------------------------------------- diff --git a/src/vip_tests/workbench/test_session_capacity.py b/src/vip_tests/workbench/test_session_capacity.py index ee36a48b..244568da 100644 --- a/src/vip_tests/workbench/test_session_capacity.py +++ b/src/vip_tests/workbench/test_session_capacity.py @@ -16,6 +16,8 @@ from __future__ import annotations +from dataclasses import dataclass + import pytest from playwright.sync_api import Page, expect from pytest_bdd import scenarios, then, when @@ -23,6 +25,8 @@ from vip_tests.workbench.conftest import ( TIMEOUT_DIALOG, TIMEOUT_QUICK, + ResourceProfileDisabled, + _option_is_disabled, _quit_vip_sessions_via_cookies, format_capacity_failure, wait_for_session_active, @@ -38,13 +42,30 @@ _SESSION_PREFIX = f"_vip_cap_{int(__import__('time').time())}_" +@dataclass(frozen=True) +class DetectedProfile: + """A resource profile discovered from the New Session dialog's dropdown. + + Disabled profiles are still reported so the caller can distinguish "no + profiles at all" from "all profiles disabled for this user" and skip with + an accurate reason. + """ + + name: str + disabled: bool + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -def _detect_profiles(page: Page) -> list[str]: - """Open the New Session dialog and read available resource profiles.""" +def _detect_profiles(page: Page) -> list[DetectedProfile]: + """Open the New Session dialog and read available resource profiles. + + Returns one :class:`DetectedProfile` per profile discovered, naming it and + recording whether it is disabled for the authenticated user. + """ page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG) dialog = page.locator(NewSessionDialog.DIALOG) @@ -64,11 +85,12 @@ def _detect_profiles(page: Page) -> list[str]: options = page.locator("[role='option']") options.first.wait_for(state="visible", timeout=TIMEOUT_QUICK) count = options.count() - profiles = [] + profiles: list[DetectedProfile] = [] for i in range(count): - text = (options.nth(i).text_content() or "").strip() + option = options.nth(i) + text = (option.text_content() or "").strip() if text: - profiles.append(text) + profiles.append(DetectedProfile(name=text, disabled=_option_is_disabled(option))) # Close the dropdown, then close the dialog via Escape. page.keyboard.press("Escape") @@ -83,7 +105,11 @@ def _launch_session( session_name: str, profile: str | None = None, ) -> None: - """Open the New Session dialog, optionally select a resource profile, and launch.""" + """Open the New Session dialog, optionally select a resource profile, and launch. + + Raises ``ResourceProfileDisabled`` if the selected profile is disabled for + the authenticated user. + """ page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG) dialog = page.locator(NewSessionDialog.DIALOG) @@ -109,6 +135,15 @@ def _launch_session( profile_dropdown.click() page.wait_for_timeout(500) option = page.locator(f"[role='option']:has-text('{profile}')").first + option.wait_for(state="visible", timeout=TIMEOUT_QUICK) + if _option_is_disabled(option): + # Profile is offered but disabled for this user (e.g. a + # group-restricted profile). Clicking would just block + # until timeout, so close the dialog and signal the caller. + page.keyboard.press("Escape") + page.keyboard.press("Escape") + expect(dialog).to_be_hidden(timeout=TIMEOUT_DIALOG) + raise ResourceProfileDisabled(profile) option.click(timeout=TIMEOUT_QUICK) else: pytest.skip(f"Resource profile dropdown not available; cannot select '{profile}'") @@ -164,7 +199,15 @@ def launch_sessions(page: Page, vip_config): # Auto-detect from the dropdown. detected = _detect_profiles(page) if detected: - profiles_to_test = detected + enabled = [p.name for p in detected if not p.disabled] + if not enabled: + # Every profile is offered but disabled for this user — nothing + # is launchable, so there is no capacity to exercise. + names = ", ".join(p.name for p in detected) + pytest.skip( + f"All resource profiles are disabled for the authenticated user: {names}" + ) + profiles_to_test = enabled else: # No profiles dropdown — launch with default. profiles_to_test = [None] @@ -173,12 +216,36 @@ def launch_sessions(page: Page, vip_config): session_count = 1 all_sessions: list[dict[str, str | None]] = [] + disabled_profiles: list[str] = [] for profile in profiles_to_test: + profile_disabled = False for i in range(session_count): label = profile or "default" name = f"{_SESSION_PREFIX}{label}_{i}" - _launch_session(page, name, profile) + try: + _launch_session(page, name, profile) + except ResourceProfileDisabled as exc: + # Configured profile the current user cannot launch. Treat as + # an environment condition (entitlement/group restriction): + # record it and move on to the remaining profiles rather than + # aborting the whole scenario. + disabled_profiles.append(exc.profile) + profile_disabled = True + break all_sessions.append({"name": name, "profile": profile}) + if profile_disabled: + continue + + if not all_sessions and disabled_profiles: + # No configured profile was launchable — there is no capacity to + # exercise. Skip rather than fail so the scenario is reported as + # skipped (not passed) on a correctly-restricted test account, + # distinct from an actual capacity failure. + names = ", ".join(disabled_profiles) + pytest.skip( + f"Resource profile(s) '{names}' are disabled for the authenticated " + "user (likely a group/entitlement restriction)" + ) return all_sessions diff --git a/src/vip_tests/workbench/test_session_capacity_k8s.py b/src/vip_tests/workbench/test_session_capacity_k8s.py index e4b3c13c..727ec412 100644 --- a/src/vip_tests/workbench/test_session_capacity_k8s.py +++ b/src/vip_tests/workbench/test_session_capacity_k8s.py @@ -26,6 +26,8 @@ from vip_tests.workbench.conftest import ( TIMEOUT_DIALOG, TIMEOUT_QUICK, + ResourceProfileDisabled, + _option_is_disabled, _quit_vip_sessions_via_cookies, wait_for_session_active, ) @@ -46,7 +48,11 @@ def _launch_session(page: Page, session_name: str, profile: str | None = None) -> None: - """Open the New Session dialog and launch with an optional resource profile.""" + """Open the New Session dialog and launch with an optional resource profile. + + Raises ``ResourceProfileDisabled`` if the selected profile is disabled for + the authenticated user. + """ page.locator(Homepage.NEW_SESSION_BUTTON).first.click(timeout=TIMEOUT_DIALOG) dialog = page.locator(NewSessionDialog.DIALOG) @@ -68,6 +74,15 @@ def _launch_session(page: Page, session_name: str, profile: str | None = None) - profile_dropdown.click() page.wait_for_timeout(500) option = page.locator(f"[role='option']:has-text('{profile}')").first + option.wait_for(state="visible", timeout=TIMEOUT_QUICK) + if _option_is_disabled(option): + # Profile is offered but disabled for this user (e.g. a + # group-restricted profile). Clicking would just block + # until timeout, so close the dialog and signal the caller. + page.keyboard.press("Escape") + page.keyboard.press("Escape") + expect(dialog).to_be_hidden(timeout=TIMEOUT_DIALOG) + raise ResourceProfileDisabled(profile) option.click(timeout=TIMEOUT_QUICK) else: pytest.skip(f"Resource profile dropdown not available; cannot select '{profile}'") @@ -233,7 +248,13 @@ def launch_profiled_session(page: Page, vip_config) -> list[dict]: profile_map = vip_config.workbench.kubernetes.node_pool_profiles profile = next(iter(profile_map.values())) name = f"{_SESSION_PREFIX}prof_0" - _launch_session(page, name, profile=profile) + try: + _launch_session(page, name, profile=profile) + except ResourceProfileDisabled as exc: + pytest.skip( + f"Resource profile '{exc.profile}' is disabled for the " + "authenticated user (likely a group/entitlement restriction)" + ) return [{"name": name, "profile": profile}] @@ -248,7 +269,13 @@ def launch_limited_session(page: Page, vip_config) -> list[dict]: ) profile = profiles[0] if profiles else None name = f"{_SESSION_PREFIX}lim_0" - _launch_session(page, name, profile=profile) + try: + _launch_session(page, name, profile=profile) + except ResourceProfileDisabled as exc: + pytest.skip( + f"Resource profile '{exc.profile}' is disabled for the " + "authenticated user (likely a group/entitlement restriction)" + ) return [{"name": name, "profile": profile}]