07.01 — Arborescence#

src/
├── main.py                                                     # entry point (cf. README)
│
├── api/                                                        # clients HTTP externes
│   ├── retrieve_dashboard_otp_code.py                          # filter + sort + pick OTP
│   ├── get_otp_history.py                                      # HTTP GET /api/otp-history
│   └── constants/endpoints.py                                  # URLs des endpoints tests-workers
│
├── caches/                                                     # cache L1 in-memory
│   ├── l1.py                                                   # dogpile.cache.memory TTL 30m
│   └── reserve_free_cache_key.py                               # UUID + threading.Lock
│
├── constants/                                                  # constantes
│   ├── pages/
│   │   ├── homepage.py                                         # HOMEPAGE_URL
│   │   ├── dashboard.py                                        # DASHBOARD_URL + ids des inputs
│   │   ├── 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/                          # fonctions (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                                  # ⭐ adapter act (hook on_failure)
│       │   │   │   ├── env_getters.py                          # ⭐ adapter EnvGetters typé
│       │   │   │   └── match_page.py                           # ⭐ adapter match_page
│       │   │   └── selenium/
│       │   │       ├── test_suite.py                           # ⭐ adapter TestSuite
│       │   │       ├── test_campaign.py                        # ⭐ adapter TestCampaign
│       │   │       ├── cli_getters.py                          # getters typés depuis le CliStore
│       │   │       ├── logs.py                                 # create_just_log_error, etc.
│       │   │       └── screenshotter.py                        # take_screenshot helper
│       │   └── regex/error_page.py                             # ERROR_PAGE_REGEX
│       ├── redis/
│       │   └── client.py                                       # Singleton client Redis
│       └── 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/                                           # fichiers à uploader
│   ├── dashboard/
│   │   ├── login.py                                            # ⭐ avec 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

Sémantique des dossiers#

DossierRôleConvention
pages/POMs (héritent SeleniumTitleMixin, POMBase)Une classe par page, fluent (return self)
lib/connectors/test_steps/actions/Connectors (TPOM) -> TPOMFonctions pures ; si paramètres, closures
lib/custom_errors/Exceptions projetSous-classes d’Exception
lib/ext/ocarina/adapters/Adapters au-dessus d’OcarinaCf. 02-adapters.md
lib/ext/redis/Wrapper Singleton RedisPour les locks distribués
lib/ext/selenium/Extensions Selenium spécifiques au projetHumanizedDriver, Watcher callback
caches/Cache L1 in-memorydogpile.cache.memory + UUID reservation
constants/ConstantesURLs, redis keys, transient errors
api/Clients HTTP externesOTP retrieval, OTP history
tests/Hiérarchie ISTQBcycles / campaigns / suites / scenarios

lib/ vs pages/#

  1. pages/ : POMs, encapsulent l’état d’une page (locators, méthodes d’action).
  2. lib/connectors/test_steps/actions/ : connectors, fonctions qui réfèrent aux POMs.
act(on_homepage, open_homepage)
#                ^^^^^^^^^^^^^
#                connector défini dans lib/connectors/test_steps/actions/homepage.py
def open_homepage(p: Homepage) -> Homepage:
    return p.open()

Le CLAUDE.md du projet IA en fait une discipline :

Log factories, never inline lambdas. .failure(log_error("msg")).success(log_success("msg")).

Et pour les connectors :

A connector function must be used in at least one scenario; speculative ones are caught in review.

tests/scenarios/ vs tests/suites/ vs tests/campaigns/ vs tests/cycles/#

DossierFichiersType
scenarios/(driver, logger) -> Scenariofactories de scénario
suites/(*, drivers_pool) -> TestSuitefactories de suite
campaigns/(*, drivers_pool) -> TestCampaignfactories de campagne
cycles/(drivers_pool) -> TestCyclefactories de cycle

Chaque niveau ne dépend que de celui en dessous.

Levels dans randomness/#

randomness/
├── level_1/{random_error_page,random_loaders_page}.py     # erreurs simples
├── level_2/{dsed,madness}.py                              # erreurs avec branches
├── level_3/chaotic_form.py                                # chaos UI (HumanizedDriver + Watcher)
└── level_4/walkthrough.py                                 # parcours accumulant plusieurs pages très aléatoires