02.08 — POMBase#
Source file:
src/ocarina/pom/base.pyFramework-agnostic abstract base for the Page Object Model. Two required methods. Zero mention of Selenium.
Code#
class POMBase(ABC):
@abstractmethod
def verify(self, *, timeout: float | None = None) -> Self:
...
@abstractmethod
def get_current_title(self) -> str:
...Six bytes of contract (the two signatures). That’s all.
Why two methods#
verify(timeout=None) -> Self#
Purpose: prove we’re on the right page. Otherwise, raise.
timeoutlets you wait up to a given time for the page to load (handy for SeleniumWebDriverWait).Nonedefault lets the implementation decide (typically readingget_timeout()which reads the CLI).- Returning
Selfenables fluent method chaining (MyPage(...).open().verify().click()).
The only method test-side that’s truly needed to guarantee “I’m on the page I want.” Everything else (click_xxx, enter_xxx) is on the subclass.
get_current_title() -> str#
Purpose: retrieve the current page’s title.
Uses:
Logging: you can write
logger.info(f"Current page: {page.get_current_title()}").Error-page detection in
act’son_failurehook (see03-railway/05-create-act-hooks.md). Example fromocarina-example:def failure_hook(pom: TPOM, exc: Exception) -> Fail: title = pom.get_current_title() if title and ERROR_PAGE_REGEX.match(title.strip()): return Fail(error=HttpErrorPageReachedError(f"HTTP error page: {title}")) return Fail(error=exc)
→ The hook doesn’t know what a WebDriver is, but knows how to call get_current_title(). Right level of abstraction.
User-side implementation#
@final
class Homepage(SeleniumTitleMixin, POMBase):
def __init__(self, *, driver: WebDriver, url: str = HOMEPAGE_URL) -> None:
self._driver = driver
self._URL = url
def open(self) -> Homepage:
self._driver.get(self._URL)
return self
def verify(self, *, timeout: float | None = None) -> Homepage:
try:
if timeout is None:
timeout = get_timeout()
WebDriverWait(self._driver, timeout).until(ec.title_is("Welcome to my homepage"))
WebDriverWait(self._driver, timeout).until(
ec.text_to_be_present_in_element((By.TAG_NAME, "h1"), "My homepage")
)
except TimeoutException as exc:
raise PageVerificationError from exc
return selfSeleniumTitleMixinprovidesget_current_titleso subclasses don’t duplicate it (see10-infra/05-selenium-adapters.md,mixins.py).verifyraisesPageVerificationError, a framework-specificExceptionsubclass. Lets transient_errors match it.return selfevery time — fluent chaining.
Why framework-agnostic#
# Selenium
class LoginPage(POMBase):
def __init__(self, driver: WebDriver):
self._driver = driver
def verify(self, timeout=None) -> Self:
WebDriverWait(self._driver, timeout or 10).until(
EC.presence_of_element_located((By.ID, "login-form"))
)
return self
# Playwright
class LoginPage(POMBase):
def __init__(self, page: Page):
self._page = page
def verify(self, timeout=None) -> Self:
self._page.wait_for_selector("#login-form", timeout=timeout)
return selfThe PlaywrightTitleMixin is no longer hypothetical — since 1.1.3 Ocarina ships it in pom/playwright/, right next to SeleniumTitleMixin. Write a PuppeteerTitleMixin the same way if you need one.
Self (PEP 673)#
@final
class CorsicamonEnterApiKeyPage(SeleniumTitleMixin, POMBase):
def enter_api_key(self) -> CorsicamonEnterApiKeyPage: # naive version
...vs
@final
class CorsicamonEnterApiKeyPage(SeleniumTitleMixin, POMBase):
def enter_api_key(self) -> Self: # better
...The Holy Book (chapter “First steps”, section “Returning self”) notes:
Every action method returns
self. It’s a deliberate design choice in Ocarina, to be respected systematically; it enables call chaining and the fluent composition of scenarios.
verify is not a matcher#
Explicit distinction in the Holy Book (chapter on scenarios composability, section “match_page”):
It’s also not recommended to disguise a
verifyas a matcher: they are two different tools.
| Tool | Goal | On failure |
|---|---|---|
verify | Guarantee you’re on the right page | Raises (PageVerificationError) |
Matcher (used by match_page) | Pick a conditional branch | Returns False |
__init__#
Convention across the entire ecosystem:
def __init__(self, *, driver: WebDriver, url: str = DEFAULT_URL) -> None:ocarina-with-ai-example’s CLAUDE.md formalizes it as convention:
POMs take a single
url: str = <DEFAULT_URL>parameter, defaulted to the constant. Scenarios construct pages with justPage(driver=driver)and passurl=...only to override.