feat(e2e): add BasePage and 7 page objects
BasePage provides navigation, error capture, and interactable discovery with fnmatch-based expected_failure matching. SettingsPage extends it with tab-aware discovery. All conftest imports are deferred to method bodies so the module loads without a live browser fixture.
This commit is contained in:
parent
20c776260f
commit
0cdd97f1c0
8 changed files with 147 additions and 0 deletions
4
tests/e2e/pages/apply_page.py
Normal file
4
tests/e2e/pages/apply_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class ApplyPage(BasePage):
|
||||
nav_label = "Apply Workspace"
|
||||
79
tests/e2e/pages/base_page.py
Normal file
79
tests/e2e/pages/base_page.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"""Base page object — navigation, error capture, interactable discovery."""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import warnings
|
||||
import fnmatch
|
||||
from dataclasses import dataclass
|
||||
|
||||
from playwright.sync_api import Page
|
||||
|
||||
from tests.e2e.models import ErrorRecord, ModeConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INTERACTABLE_SELECTORS = [
|
||||
'[data-testid="baseButton-primary"] button',
|
||||
'[data-testid="baseButton-secondary"] button',
|
||||
'[data-testid="stTab"] button[role="tab"]',
|
||||
'[data-testid="stSelectbox"]',
|
||||
'[data-testid="stCheckbox"] input',
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class InteractableElement:
|
||||
label: str
|
||||
selector: str
|
||||
index: int
|
||||
|
||||
|
||||
class BasePage:
|
||||
"""Base page object for all Peregrine pages."""
|
||||
|
||||
nav_label: str = ""
|
||||
|
||||
def __init__(self, page: Page, mode: ModeConfig, console_messages: list):
|
||||
self.page = page
|
||||
self.mode = mode
|
||||
self._console_messages = console_messages
|
||||
|
||||
def navigate(self) -> None:
|
||||
"""Navigate to this page by clicking its sidebar nav link."""
|
||||
from tests.e2e.conftest import wait_for_streamlit
|
||||
sidebar = self.page.locator('[data-testid="stSidebarNav"]')
|
||||
sidebar.get_by_text(self.nav_label, exact=False).first.click()
|
||||
wait_for_streamlit(self.page)
|
||||
|
||||
def get_errors(self) -> list[ErrorRecord]:
|
||||
from tests.e2e.conftest import get_page_errors
|
||||
return get_page_errors(self.page)
|
||||
|
||||
def get_console_errors(self) -> list[str]:
|
||||
from tests.e2e.conftest import get_console_errors
|
||||
return get_console_errors(self._console_messages)
|
||||
|
||||
def discover_interactables(self, skip_sidebar: bool = True) -> list[InteractableElement]:
|
||||
"""Find all interactive elements on the current page, excluding sidebar."""
|
||||
found: list[InteractableElement] = []
|
||||
|
||||
for selector in INTERACTABLE_SELECTORS:
|
||||
elements = self.page.query_selector_all(selector)
|
||||
for i, el in enumerate(elements):
|
||||
if skip_sidebar and el.evaluate(
|
||||
"el => el.closest('[data-testid=\"stSidebar\"]') !== null"
|
||||
):
|
||||
continue
|
||||
label = (el.inner_text() or el.get_attribute("aria-label") or f"element-{i}").strip()
|
||||
label = label[:80]
|
||||
found.append(InteractableElement(label=label, selector=selector, index=i))
|
||||
|
||||
for pattern in self.mode.expected_failures:
|
||||
matches = [e for e in found if fnmatch.fnmatch(e.label, pattern)]
|
||||
if len(matches) > 1:
|
||||
warnings.warn(
|
||||
f"expected_failure pattern '{pattern}' matches {len(matches)} elements: "
|
||||
+ ", ".join(f'"{m.label}"' for m in matches),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return found
|
||||
4
tests/e2e/pages/home_page.py
Normal file
4
tests/e2e/pages/home_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class HomePage(BasePage):
|
||||
nav_label = "Home"
|
||||
4
tests/e2e/pages/interview_prep_page.py
Normal file
4
tests/e2e/pages/interview_prep_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class InterviewPrepPage(BasePage):
|
||||
nav_label = "Interview Prep"
|
||||
4
tests/e2e/pages/interviews_page.py
Normal file
4
tests/e2e/pages/interviews_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class InterviewsPage(BasePage):
|
||||
nav_label = "Interviews"
|
||||
4
tests/e2e/pages/job_review_page.py
Normal file
4
tests/e2e/pages/job_review_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class JobReviewPage(BasePage):
|
||||
nav_label = "Job Review"
|
||||
44
tests/e2e/pages/settings_page.py
Normal file
44
tests/e2e/pages/settings_page.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Settings page — tab-aware page object."""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from tests.e2e.pages.base_page import BasePage, InteractableElement
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsPage(BasePage):
|
||||
nav_label = "Settings"
|
||||
|
||||
def discover_interactables(self, skip_sidebar: bool = True) -> list[InteractableElement]:
|
||||
"""
|
||||
Settings has multiple tabs. Click each expected tab, collect interactables,
|
||||
return the full combined list.
|
||||
"""
|
||||
from tests.e2e.conftest import wait_for_streamlit
|
||||
|
||||
all_elements: list[InteractableElement] = []
|
||||
tab_labels = self.mode.settings_tabs
|
||||
|
||||
for tab_label in tab_labels:
|
||||
# Match on full label text — handles emoji correctly.
|
||||
# Do NOT use tab_label.split()[-1]: "My Profile" and "Resume Profile"
|
||||
# both end in "Profile", causing a silent collision.
|
||||
tab_btn = self.page.locator(
|
||||
'[data-testid="stTab"] button[role="tab"]'
|
||||
).filter(has_text=tab_label)
|
||||
if tab_btn.count() == 0:
|
||||
log.warning("Settings tab not found: %s", tab_label)
|
||||
continue
|
||||
tab_btn.first.click()
|
||||
wait_for_streamlit(self.page)
|
||||
|
||||
tab_elements = super().discover_interactables(skip_sidebar=skip_sidebar)
|
||||
# Exclude tab buttons (already handled by clicking)
|
||||
tab_elements = [
|
||||
e for e in tab_elements
|
||||
if 'role="tab"' not in e.selector
|
||||
]
|
||||
all_elements.extend(tab_elements)
|
||||
|
||||
return all_elements
|
||||
4
tests/e2e/pages/survey_page.py
Normal file
4
tests/e2e/pages/survey_page.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from tests.e2e.pages.base_page import BasePage
|
||||
|
||||
class SurveyPage(BasePage):
|
||||
nav_label = "Survey Assistant"
|
||||
Loading…
Reference in a new issue