"""Web Page Object 基类 — 封装所有页面共享的通用操作 (Playwright)。 每个具体页面应继承 BasePage,在 __init__ 中声明所有定位器。 Usage: class LoginPage(BasePage): def __init__(self, page): super().__init__(page) self.phone_input = page.get_by_placeholder("请输入用户名") self.login_btn = page.get_by_role("button", name="登录") """ import time from datetime import datetime from typing import Optional from playwright.sync_api import Page, Locator, expect from tests.core.allure_support import attach_png from tests.core.paths import SCREENSHOTS_DIR class BasePage: """Web Page Object 基类。""" def __init__(self, page: Page, timeout_ms: int = 30_000): self.page = page self.timeout = timeout_ms # ── 多候选定位 ── def first_matching(self, *locators: Locator, timeout_ms: int = 5_000) -> Locator: """依次尝试多个候选定位器,返回第一个实际命中的;全部落空则抛错。 用于"语义文本锚点优先、结构路径兜底"的健壮定位: 页面文案通常比 DOM 结构稳定,文案锚点命中就用它, 结构变了还有旧路径兜底。 """ deadline = time.monotonic() + timeout_ms / 1000 while True: for loc in locators: try: if loc.count() > 0 and loc.first.is_visible(): return loc except Exception: continue if time.monotonic() >= deadline: break self.page.wait_for_timeout(200) raise TimeoutError( f"所有候选定位器在 {timeout_ms}ms 内均未命中 " f"({len(locators)} 个候选)" ) # ── 通用等待 ── def wait_visible(self, locator: Locator, timeout_ms: Optional[int] = None) -> None: """等待元素可见。""" expect(locator).to_be_visible(timeout=timeout_ms or self.timeout) def wait_hidden(self, locator: Locator, timeout_ms: Optional[int] = None) -> None: """等待元素消失(如 Loading 遮罩)。""" expect(locator).to_be_hidden(timeout=timeout_ms or self.timeout) def wait_for_text(self, text: str, timeout_ms: Optional[int] = None) -> None: """等待页面上出现指定文本。""" expect(self.page.get_by_text(text)).to_be_visible(timeout=timeout_ms or self.timeout) # ── 通用操作 ── def fill_field(self, locator: Locator, value: str) -> None: """安全填写输入框(先等待可见,再清空后填写)。""" self.wait_visible(locator) locator.clear() locator.fill(value) def click_when_ready(self, locator: Locator) -> None: """安全点击(等待可见 + 可操作)。""" self.wait_visible(locator) locator.click() def get_text(self, locator: Locator) -> str: """获取元素的 inner text。""" self.wait_visible(locator) return locator.inner_text() # ── 截图 ── def screenshot(self, name: str, full_page: bool = False) -> str: """关键节点截图(存盘 + attach Allure),返回文件路径。""" scenario_dir = SCREENSHOTS_DIR / self.__class__.__name__ scenario_dir.mkdir(parents=True, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") path = str(scenario_dir / f"{name}_{ts}.png") self.page.screenshot(path=path, full_page=full_page) attach_png(path, name=name) return path # ── 导航 ── def navigate(self, url: str) -> None: """导航到指定 URL 并等待页面加载完成。""" self.page.goto(url, wait_until="domcontentloaded")