07.01 — Tree

07.01 — Tree#

src/
├── main.py                                                     # entry point (see README)
│
├── api/                                                        # external HTTP clients
│   ├── retrieve_dashboard_otp_code.py                          # filter + sort + pick OTP
│   ├── get_otp_history.py                                      # HTTP GET /api/otp-history
│   └── constants/endpoints.py                                  # URLs of tests-workers endpoints
│
├── caches/                                                     # in-memory L1 cache
│   ├── l1.py                                                   # dogpile.cache.memory TTL 30m
│   └── reserve_free_cache_key.py                               # UUID + threading.Lock
│
├── constants/                                                  # constants
│   ├── pages/
│   │   ├── homepage.py                                         # HOMEPAGE_URL
│   │   ├── dashboard.py                                        # DASHBOARD_URL + input ids
│   │   ├── random_loaders.py
│   │   ├── sacred_upload.py
│   │   ├── corsicamon.py
│   │   ├── chaotic_form.py
│   │   ├── madness.py
│   │   ├── random_error_page.py
│   │   └── donkey_sausage_eater_detector.py
│   └── sys/
│       ├── redis_keys.py                                       # OTP_SEND_LOCK_KEY, ...
│       └── transient_errors.py                                 # (WebDriverException, HttpError...)
│
├── lib/
│   ├── connectors/test_steps/actions/                          # functions (TPOM) -> TPOM
│   │   ├── homepage.py
│   │   ├── dashboard_login.py                                  # ~10 connectors (with/without retries)
│   │   ├── dashboard_welcome.py
│   │   ├── dashboard_protected_page.py
│   │   ├── sacred_upload.py
│   │   ├── corsicamon_enter_api_key.py
│   │   ├── corsicamon_main.py
│   │   ├── chaotic_form.py
│   │   ├── random_error.py
│   │   ├── random_loaders.py
│   │   ├── madness.py
│   │   ├── this_is_bastia.py
│   │   ├── cors_errors.py
│   │   ├── bsod.py
│   │   ├── dsed.py
│   │   └── ids_bypassed.py
│   ├── custom_errors/
│   │   ├── http.py                                             # HttpErrorPageReachedError
│   │   └── transient_error.py                                  # TransientError
│   └── ext/                                                    # extensions / adapters
│       ├── ocarina/
│       │   ├── adapters/
│       │   │   ├── agnostic/
│       │   │   │   ├── act.py                                  # ⭐ act adapter (on_failure hook)
│       │   │   │   ├── env_getters.py                          # ⭐ typed EnvGetters adapter
│       │   │   │   └── match_page.py                           # ⭐ match_page adapter
│       │   │   └── selenium/
│       │   │       ├── test_suite.py                           # ⭐ TestSuite adapter
│       │   │       ├── test_campaign.py                        # ⭐ TestCampaign adapter
│       │   │       ├── cli_getters.py                          # typed getters from CliStore
│       │   │       ├── logs.py                                 # create_just_log_error, etc.
│       │   │       └── screenshotter.py                        # take_screenshot helper
│       │   └── regex/error_page.py                             # ERROR_PAGE_REGEX
│       ├── redis/
│       │   └── client.py                                       # Redis Singleton client
│       └── selenium/
│           ├── humanize/                                       # ⭐ HumanizedDriver + keyboard
│           │   ├── proxy.py
│           │   └── keyboard.py
│           ├── pages/verify_elements_presence.py
│           └── watchers/catch_me_if_you_can_watcher.py         # ⭐ Watcher callback
│
├── pages/                                                      # POMs (Selenium + SeleniumTitleMixin)
│   ├── homepage.py
│   ├── random_loaders.py
│   ├── chaotic_form.py
│   ├── random_error.py
│   ├── sacred_upload/
│   │   ├── sacred_upload.py
│   │   └── fixtures/                                           # files to upload
│   ├── dashboard/
│   │   ├── login.py                                            # ⭐ with OTP, retries, redis locks
│   │   ├── welcome_page.py
│   │   └── protected_page.py
│   ├── corsicamon/
│   │   ├── enter_api_key.py
│   │   └── main.py
│   ├── madness/
│   │   ├── base.py
│   │   ├── matchers.py                                         # has_cors, has_this_is_bastia
│   │   ├── cors.py
│   │   └── this_is_bastia.py
│   └── donkey_sausage_detector/
│       └── ids_bypassed.py
│
└── tests/
    ├── cycles/e2e.py                                           # ⭐ TestCycle (smoke + main)
    ├── campaigns/
    │   ├── global_smoke_tests.py                               # smoke
    │   ├── corsicamon.py                                       # main (+ smoke variant)
    │   ├── dashboard_login.py                                  # main
    │   ├── randomness.py                                       # main
    │   └── sacred_upload.py                                    # main
    ├── suites/
    │   ├── global_smoke_tests.py
    │   ├── randomness.py
    │   ├── dashboard/
    │   │   ├── access/
    │   │   │   ├── happy_paths.py
    │   │   │   └── unhappy_paths.py
    │   │   └── data_driven/multi_login.py
    │   ├── sacred_upload/{happy_paths,unhappy_paths}.py
    │   └── corsicamon/{smoke_tests,happy_paths,unhappy_paths}.py
    └── scenarios/
        ├── homepage/verify_homepage.py
        ├── dashboard/
        │   ├── access/{happy_paths,unhappy_paths}.py
        │   ├── back_to_igoristan.py
        │   └── data_driven/{multi_login,datasets/multi_login}.py
        ├── sacred_upload/{upload_files,just_go_back_to_igoristan}.py
        ├── corsicamon/{enter_api_key,new_draw,add_corsicamon,back_to_igoristan}.py
        └── randomness/
            ├── level_1/{random_error_page,random_loaders_page}.py
            ├── level_2/{dsed,madness}.py
            ├── level_3/chaotic_form.py
            └── level_4/walkthrough.py

Folder semantics#

FolderRoleConvention
pages/POMs (inherit SeleniumTitleMixin, POMBase)One class per page, fluent (return self)
lib/connectors/test_steps/actions/Connectors (TPOM) -> TPOMPure functions; if parameters, closures
lib/custom_errors/Project exceptionsException subclasses
lib/ext/ocarina/adapters/Adapters above OcarinaSee 02-adapters.md
lib/ext/redis/Redis Singleton wrapperFor distributed locks
lib/ext/selenium/Selenium extensions specific to the projectHumanizedDriver, Watcher callback
caches/In-memory L1 cachedogpile.cache.memory + UUID reservation
constants/ConstantsURLs, redis keys, transient errors
api/External HTTP clientsOTP retrieval, OTP history
tests/ISTQB hierarchycycles / campaigns / suites / scenarios

lib/ vs pages/#

  1. pages/: POMs — encapsulate a page’s state (locators, action methods).
  2. lib/connectors/test_steps/actions/: connectors — functions that call into POMs.
act(on_homepage, open_homepage)
#                ^^^^^^^^^^^^^
#                connector defined in lib/connectors/test_steps/actions/homepage.py
def open_homepage(p: Homepage) -> Homepage:
    return p.open()

The AI project’s CLAUDE.md enforces this as a discipline:

07.02 — The 5 adapters

07.02 — The 5 adapters#

Every Ocarina project starts by writing five adapters on top of the framework.
The ocarina-example project provides them as reference.

Listing#

AdapterStatusFilePurpose
actrequiredlib/ext/ocarina/adapters/agnostic/act.pyProject-specific on_failure hook
match_pageoptionallib/ext/ocarina/adapters/agnostic/match_page.pyShared exception policy
EnvGettersoptionallib/ext/ocarina/adapters/agnostic/env_getters.pyTyped access to env vars
TestSuiterequiredlib/ext/ocarina/adapters/selenium/test_suite.pyFaçade: pins transient_errors, max_retries, autoscreen
TestCampaignrequiredlib/ext/ocarina/adapters/selenium/test_campaign.pyFaçade: max_workers from CLI

act#

# lib/ext/ocarina/adapters/agnostic/act.py
from contextlib import suppress
from ocarina.dsl.testing_with_railway.constructors.create_act import create_act
from ocarina.railway.result import Fail
from lib.custom_errors.http import HttpErrorPageReachedError
from lib.ext.ocarina.regex.error_page import ERROR_PAGE_REGEX


def act(pom: TPOM, action: Callable[[TPOM], TPOM]) -> ActionStart[TPOM]:
    def failure_hook(pom: TPOM, exc: Exception) -> Fail:
        with suppress(Exception):
            title = pom.get_current_title()
            is_http_error_page = title and ERROR_PAGE_REGEX.match(title.strip())
            if is_http_error_page:
                http_error = HttpErrorPageReachedError(f"HTTP error page: {title}")
                http_error.__cause__ = exc
                return Fail(error=http_error)
        return Fail(error=exc)

    return create_act(pom, action, on_failure=failure_hook)
  1. get_current_title(): uses POMBase’s mandatory method (see ../02-ocarina/08-pom-base.md).
  2. ERROR_PAGE_REGEX: detects titles shaped like HTTP error pages (^\d{3}\s.*500 Internal Server Error).
  3. HttpErrorPageReachedError: project exception, included in transient_errors.

This adapter is what makes error-page replay automatic.

07.03 — Dashboard login scenarios

07.03 — Dashboard login scenarios#

Three families: happy paths, unhappy paths, data-driven multi-login. All exercise the 10%-fail useAuth and OTP coordination.

dashboard_login#

# src/tests/campaigns/dashboard_login.py
def create_igoristan_login_campaign(*, drivers_pool: SeleniumWebDriversPool) -> TestCampaign:
    return TestCampaign(
        name="Dashboard login",
        suites=[
            create_igoristan_login_happy_paths_test_suite(drivers_pool=drivers_pool),
            create_igoristan_login_unhappy_paths_test_suite(drivers_pool=drivers_pool),
            create_igoristan_login_data_driven_test_suite(drivers_pool=drivers_pool),
        ],
    )

Happy paths#

# src/tests/suites/dashboard/access/happy_paths.py
def create_igoristan_login_happy_paths_test_suite(*, drivers_pool) -> TestSuite:
    return TestSuite(
        name="Login happy paths",
        tests=[
            test_dashboard_login_without_otp_happy_path,
            test_dashboard_login_with_otp_happy_path,
            test_dashboard_login_page_back_to_igoristan_button,
        ],
        drivers_pool=drivers_pool,
    )

Test 1: login without OTP#

def dashboard_login_without_otp_happy_path(driver, logger):
    dashboard_creds = create_env_getters().get_credentials("dashboard")
    on_dashboard_login_page = DashboardLoginPage(driver=driver)
    on_dashboard_welcome_page = DashboardWelcomePage(driver=driver)

    just_log_error = create_just_log_error(logger=logger)
    log_error_with_current_url = create_log_error_with_current_url(logger=logger, driver=driver)
    just_log_success = create_just_log_success(logger=logger)
    log_success_with_current_url_and_take_screenshot = create_log_success_with_current_url_and_take_screenshot(...)

    retries_amount = max(get_max_workers(), 10)

    return [
        drive_page(
            act(on_dashboard_login_page, open_dashboard_login_page)
                .failure(just_log_error("Failed to open the dashboard login page..."))
                .success(just_log_success("Opened the dashboard login page!")),
            act(on_dashboard_login_page, verify_dashboard_login_page)
                .failure(log_error_with_current_url("Failed to verify..."))
                .success(log_success_with_current_url_and_take_screenshot("Verified!")),
            act(on_dashboard_login_page,
                login_without_otp_and_with_retries(dashboard_creds, retries_amount, logger=logger))
                .failure(just_log_error("Failed to connect to the dashboard without OTP..."))
                .success(just_log_success("Connected to the dashboard!")),
        ),
        drive_page(
            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)
                .failure(log_error_with_current_url("Failed to verify..."))
                .success(log_success_with_current_url_and_take_screenshot("Verified!")),
        ),
    ]
  1. retries_amount = max(get_max_workers(), 10): at least 10 internal login retries to beat the 10% random failure.
  2. login_without_otp_and_with_retries(creds, retries_amount, logger=logger): closure capturing creds, retry count, and logger. Returns Callable[[DashboardLoginPage], DashboardLoginPage].
  3. 2 drive_pages: login page then welcome page. Each opens, verifies, screenshots.
  4. Log factories everywhere: no inline lambdas.
  5. SeleniumTitleMixin: get_current_title() feeds act’s on_failure hook.

Test 2: login with OTP#

def dashboard_login_with_otp_happy_path(driver, logger):
    # ... same setup ...

    cache = in_memory_cache_with_30m_ttl
    fresh_cache_key_for_username = reserve_free_cache_key(cache)
    fresh_cache_key_for_otp_send_button_click_date = reserve_free_cache_key(cache)

    return [
        drive_page(
            act(on_dashboard_login_page, open_dashboard_login_page)...,
            act(on_dashboard_login_page, verify_dashboard_login_page)...,
            act(on_dashboard_login_page,
                start_to_login_with_otp_and_with_retries(
                    dashboard_creds, retries_amount,
                    cache=cache, logger=logger,
                    username_cache_key=fresh_cache_key_for_username,
                    otp_send_button_click_date_cache_key=fresh_cache_key_for_otp_send_button_click_date,
                ))...,
            act(on_dashboard_login_page, verify_otp_screen)...,
            act(on_dashboard_login_page,
                type_otp_with_retries(
                    retries_amount,
                    cache=cache, logger=logger,
                    username_cache_key=fresh_cache_key_for_username,
                    otp_send_button_click_date_cache_key=fresh_cache_key_for_otp_send_button_click_date,
                ))...,
        ),
        drive_page(
            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)...,
            act(on_dashboard_welcome_page, click_on_go_to_nested_page_btn)...,
        ),
        drive_page(
            act(on_dashboard_protected_page, verify_dashboard_protected_page)...,
        ),
    ]

Three drive_pages, five acts in the first. L1 cache + reserved keys share values between acts (see 08-caches-locks.md).

07.04 — Corsicamon scenarios

07.04 — Corsicamon scenarios#

Igoristan’s “Corsican Pokédex (Corsicadex)” page. API key input, random draw with 1/5 fail, transient_errors for Error('lol'), and POM-internal retries against “Corsicamons loaded / network error” states.

Campaign#

# src/tests/campaigns/corsicamon.py
def create_igoristan_corsicamon_campaign(*, drivers_pool) -> TestCampaign:
    return TestCampaign(
        name="Corsicamon",
        suites=[
            create_corsicamon_happy_paths_test_suite(drivers_pool=drivers_pool),
            create_corsicamon_unhappy_paths_test_suite(drivers_pool=drivers_pool),
        ],
    )


def create_igoristan_corsicamon_smoke_campaign(*, drivers_pool) -> TestCampaign:
    return TestCampaign(
        name="Corsicamon - Smoke",
        suites=[create_corsicamon_smoke_tests_suite(drivers_pool=drivers_pool)],
    )

Smoke test#

# tests/scenarios/corsicamon/enter_api_key.py
def scenario_enter_api_key_smoke(driver, logger):
    page = CorsicamonEnterApiKeyPage(driver=driver)
    ...
    return [
        drive_page(
            act(page, open_corsicamon_page)...,
            act(page, verify_enter_api_key_screen)...,
        ),
    ]


test_enter_api_key_smoke = create_selenium_test(
    name="Corsicamon - Smoke - enter API key screen visible",
    test_scenario=lambda driver, logger: Scenario(
        test_chain=scenario_enter_api_key_smoke(driver, logger)
    ),
)

Verifies the API key entry page appears. Fails → smoke fails → main campaigns skipped.

07.05 — Randomness scenarios (4 levels)

07.05 — Randomness scenarios (4 levels)#

Four progressive levels of chaos. We start simple, ramp up complexity, stress Ocarina more and more.

Campaign#

# src/tests/campaigns/randomness.py
def create_igoristan_randomness_campaign(*, drivers_pool) -> TestCampaign:
    return TestCampaign(
        name="Randomness",
        suites=[create_randomness_test_suite(drivers_pool=drivers_pool)],
    )

Suite#

# src/tests/suites/randomness.py
def create_randomness_test_suite(*, drivers_pool) -> TestSuite:
    return TestSuite(
        name="Randomness",
        tests=[
            test_random_error_page,                     # level 1
            test_random_loaders_page,                   # level 1
            test_dsed,                                  # level 2
            test_madness,                               # level 2
            test_chaotic_form,                          # level 3
            test_walkthrough,                           # level 4
        ],
        drivers_pool=drivers_pool,
    )

Levels#

LevelMechanic exercisedDifficulty
1Base DSL (drive_page, act)easy
2match_page / whenmedium
3HumanizedDriver + Watcherhard
4Multi-page chaotic walkthroughhard

Level 1 — Random Error Page#

# tests/scenarios/randomness/level_1/random_error_page.py
def scenario_random_error_page(driver, logger):
    page = RandomErrorPage(driver=driver)
    return [
        drive_page(
            act(page, open_random_error_page)...,
            act(page, verify_random_error_page)...,
        ),
    ]


test_random_error_page = create_selenium_test(
    name="Random Error Page - smoke",
    test_scenario=lambda driver, logger: Scenario(test_chain=scenario_random_error_page(driver, logger)),
)

Deliberately flaky — exercises the on_failure hook.

07.06 — Sacred upload scenarios

07.06 — Sacred upload scenarios#

The /igoristan/sacred-upload page lets you drag-and-drop a file. Exercises the file-upload pattern on the Ocarina side.

Campaign#

# src/tests/campaigns/sacred_upload.py
def create_igoristan_sacred_upload_campaign(*, drivers_pool) -> TestCampaign:
    return TestCampaign(
        name="Sacred upload",
        suites=[
            create_sacred_upload_happy_paths_test_suite(drivers_pool=drivers_pool),
            create_sacred_upload_unhappy_paths_test_suite(drivers_pool=drivers_pool),
        ],
    )

Scenario: upload files#

# tests/scenarios/sacred_upload/upload_files.py
def scenario_upload_files(driver, logger):
    page = SacredUploadPage(driver=driver)
    fixture_dir = Path(__file__).parent.parent.parent.parent / "pages" / "sacred_upload" / "fixtures"
    file_to_upload = fixture_dir / "sample.png"

    return [
        drive_page(
            act(page, open_sacred_upload_page)...,
            act(page, verify_sacred_upload_page)...,
            act(page, upload_file(file_to_upload))...,
            act(page, verify_file_uploaded(file_to_upload.name))...,
        ),
    ]


test_upload_files = create_selenium_test(
    name="Sacred upload - upload a file",
    test_scenario=lambda driver, logger: Scenario(test_chain=scenario_upload_files(driver, logger)),
)

<input type="file">#

class SacredUploadPage(SeleniumTitleMixin, POMBase):
    _file_input = (By.CSS_SELECTOR, "input[type='file']")
    _uploaded_preview = (By.CSS_SELECTOR, ".upload-preview")

    def upload_file(self, file_path: Path) -> SacredUploadPage:
        file_input = self._driver.find_element(*self._file_input)
        file_input.send_keys(str(file_path.resolve()))     # ← Selenium pattern: send_keys on input[type=file]
        return self

    def verify_file_uploaded(self, filename: str) -> SacredUploadPage:
        WebDriverWait(self._driver, get_timeout()).until(
            ec.text_to_be_present_in_element(self._uploaded_preview, filename)
        )
        return self

Standard Selenium pattern: send_keys(<absolute path>) on an input[type=file], even if the input is hidden. Selenium bypasses browser security for file inputs only.

07.07 — HumanizedDriver

07.07 — HumanizedDriver#

Source file: src/lib/ext/selenium/humanize/proxy.py

Proxy pattern: wraps a Selenium WebDriver and intercepts only find_element* to return WebElements whose send_keys are humanized (slow typing with typos, hesitations, corrections).

Code#

class HumanizedDriver(WebDriver):
    def __init__(self, driver: WebDriver, **keyboard_config: Unpack[KeyboardConfig]) -> None:
        object.__init__(self)
        self._driver = driver
        self._config = keyboard_config

    def find_element(self, by: str | RelativeBy = "id", value: str | None = None) -> _HumanizedWebElement:
        element = self._driver.find_element(by, value)
        return _HumanizedWebElement(element, self._config)

    def find_elements(self, by: str | RelativeBy = "id", value: str | None = None) -> list[WebElement]:
        elements = self._driver.find_elements(by, value)
        return [_HumanizedWebElement(el, self._config) for el in elements]

    def __getattr__(self, name: str):
        return getattr(self._driver, name)

1. Inherits from WebDriver#

class HumanizedDriver(WebDriver):

HumanizedDriver passes isinstance(x, WebDriver) checks.

07.08 — L1 cache + reserved keys + distributed Redis locks

07.08 — L1 cache + reserved keys + distributed Redis locks#

Internal coordination mechanics: an in-memory cache to share values within a test, a Redis lock to serialize a click across several workers.

L1 cache#

# src/caches/l1.py
from dogpile.cache import make_region

in_memory_cache_with_30m_ttl = make_region().configure(
    "dogpile.cache.memory", expiration_time=30 * 60
)

One global cache, dogpile.cache.memory backend, 30-minute TTL. Everything in RAM — no Redis for local-process state.

Why an L1 cache?#

Ocarina’s DSL is static: a scenario describes a sequence of acts. But sometimes you need to share a value between two acts:

07.09 — catch_me_if_you_can

07.09 — catch_me_if_you_can#

Detects parasite elements that pop on the page while the test is running, and traces them.

Callback#

# src/lib/ext/selenium/watchers/catch_me_if_you_can_watcher.py
def catch_me_if_you_can_cb(watcher: SeleniumWatcher) -> None:
    elements = watcher.driver.execute_script(
        "return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
    )

    if not elements:
        return

    raw = watcher.driver.execute_script(
        """
        return arguments[0].map(el => ({
            tag:       el.tagName.toLowerCase(),
            text:      el.innerText.trim(),
            id:        el.id,
            cls:       el.className,
            name:      el.getAttribute('name') || '',
            testid:    el.getAttribute('data-testid') || '',
        }));
        """,
        elements,
    )

    for attrs in raw:
        fingerprint = ":".join(
            filter(
                None,
                [
                    attrs["tag"],
                    attrs["text"],
                    attrs["id"],
                    attrs["cls"],
                    attrs["name"],
                    attrs["testid"],
                ],
            )
        )

        if fingerprint in watcher.cache:
            continue

        watcher.cache.add(fingerprint)
        watcher.report(
            f"catch-me-if-you-can element detected: <{attrs['tag']}> {attrs['text']!r}",
            label="CATCH_ME_IF_YOU_CAN",
        )

Mechanics#

1. Javascript to bypass implicit wait#

elements = watcher.driver.execute_script(
    "return Array.from(document.querySelectorAll('.catch-me-if-you-can'));"
)

Not find_elements(By.CSS_SELECTOR, ...).

07.10 — EnvGetters

07.10 — EnvGetters#

Typed accessor for environment variables. Avoids typos, refuses unknown keys at compile time, brings IDE autocomplete.

Code#

# src/lib/ext/ocarina/adapters/agnostic/env_getters.py
from typing import Literal
from types import MappingProxyType
from ocarina.opinionated.infra.env import EnvGetters, Effects

type _CredsKeys = Literal["dashboard"]
type _ValuesKeys = Literal["igor_api_key", "redis_url"]


def _load_env() -> None:
    from dotenv import load_dotenv
    load_dotenv()


_DEFAULT_EFFECTS = (_load_env,)


class _EnvGetters(EnvGetters[_CredsKeys, _ValuesKeys]):
    def __init__(self, *, effects: Effects) -> None:
        for effect in effects:
            effect()
        super().__init__(
            credentials={
                "dashboard": MappingProxyType({
                    "login": os.environ["DASH_USERNAME"],
                    "password": os.environ["DASH_PASSWORD"],
                }),
            },
            values={
                "igor_api_key": os.environ["IGOR_API_KEY"],
                "redis_url": os.environ["REDIS_URL"],
            },
        )


def create_env_getters(*, effects: Effects | None = None) -> _EnvGetters:
    if effects is None:
        effects = _DEFAULT_EFFECTS
    return _EnvGetters(effects=effects)

_CredsKeys and _ValuesKeys#

type _CredsKeys = Literal["dashboard"]
type _ValuesKeys = Literal["igor_api_key", "redis_url"]
  1. Credentials: login/password pairs (immutable dict via MappingProxyType).
  2. Values: raw values.

MappingProxyType#

"dashboard": MappingProxyType({
    "login": os.environ["DASH_USERNAME"],
    "password": os.environ["DASH_PASSWORD"],
}),

MappingProxyType is a read-only wrapper around a dict: