<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Scenarios on Ocarina — précis</title>
    <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/scenarios/</link>
    <description>Recent content in Scenarios on Ocarina — précis</description>
    <generator>Hugo</generator>
    <language>fr-FR</language>
    <lastBuildDate>Mon, 01 Jun 2026 19:25:20 +0200</lastBuildDate>
    <atom:link href="https://mojo-molotov.github.io/from-ocarina-to-igor/fr/tags/scenarios/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>02.05.01 — Test[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/01-test/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/05-orchestration/01-test/</guid>
      <description>&lt;h1 id=&#34;020501testdriver&#34;&gt;02.05.01 — &lt;code&gt;Test[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020501testdriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/dsl/testing/oc_test.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/dsl/testing/oc_test.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;signature&#34;&gt;Signature&lt;a class=&#34;anchor&#34; href=&#34;#signature&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Test&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;__init__&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name: TestName,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test_id: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test_scenario: TestScenario[Driver],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        pre_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        post_test_scenarios_fragments: Sequence[TestScenarioFragment[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        skipped: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ff79c6&#34;&gt;if&lt;/span&gt; test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;name &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; name&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;test_id &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_id&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_test_scenario &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; test_scenario&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_pre_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; pre_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_post_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; post_test_scenarios_fragments &lt;span style=&#34;color:#ff79c6&#34;&gt;or&lt;/span&gt; []&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;font-style:italic&#34;&gt;self&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;_skipped &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; skipped&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Six paramètres :&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Paramètre&lt;/th&gt;&#xA;          &lt;th&gt;Type&lt;/th&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;name&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestName&lt;/code&gt; (&lt;code&gt;str&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;Label humain ; apparaît dans le rapport, et &lt;strong&gt;devient le nom du fichier de log&lt;/strong&gt; (donc soumis à &lt;code&gt;is_valid_filename&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_id&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;str | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Identifiant stable pour &lt;code&gt;--only&lt;/code&gt; / &lt;code&gt;--exclude&lt;/code&gt;. Si absent : prend &lt;code&gt;name&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_scenario&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestScenario[Driver]&lt;/code&gt; (alias = &lt;code&gt;Callable[[Driver, ILogger], Scenario[Driver]]&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;&lt;em&gt;Factory&lt;/em&gt; qui construit le &lt;code&gt;Scenario&lt;/code&gt; au moment de l&amp;rsquo;exécution.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pre_test_scenarios_fragments&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[TestScenarioFragment[Driver]] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Fonctions &lt;code&gt;(driver, logger) -&amp;gt; TestChain&lt;/code&gt; exécutées &lt;strong&gt;avant&lt;/strong&gt; le scénario principal.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;post_test_scenarios_fragments&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Sequence[TestScenarioFragment[Driver]] | None&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Fonctions &lt;code&gt;(driver, logger) -&amp;gt; TestChain&lt;/code&gt; exécutées &lt;strong&gt;après&lt;/strong&gt; le scénario principal.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;bool&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Si &lt;code&gt;True&lt;/code&gt;, le test est enregistré mais pas exécuté.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h3 id=&#34;1-final&#34;&gt;1. &lt;code&gt;@final&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#1-final&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Pas d&amp;rsquo;héritage utilisateur. Si l&amp;rsquo;on veut un test « &lt;em&gt;spécial&lt;/em&gt; », on &lt;em&gt;compose&lt;/em&gt; via les fragments ou via un scénario, on ne sous-classe pas.&lt;/p&gt;</description>
    </item>
    <item>
      <title>04.01 — Stratégie « dehors comme un utilisateur »</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/01-strategy/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/01-strategy/</guid>
      <description>&lt;h1 id=&#34;0401stratégie-dehors-comme-un-utilisateur&#34;&gt;04.01 — Stratégie « &lt;em&gt;dehors comme un utilisateur&lt;/em&gt; »&lt;a class=&#34;anchor&#34; href=&#34;#0401strat%c3%a9gie-dehors-comme-un-utilisateur&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;C&amp;rsquo;est la posture documentée dès le &lt;code&gt;conftest.py&lt;/code&gt; des tests scénarios. Aucun test ne triche en regardant les internes.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;testsscenariosconftestpy&#34;&gt;&lt;code&gt;tests/scenarios/conftest.py&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#testsscenariosconftestpy&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Composant&lt;/th&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;FakeDriver&lt;/code&gt; (dataclass)&lt;/td&gt;&#xA;          &lt;td&gt;Driver minimaliste : &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;disposed&lt;/code&gt;. Aucune méthode Selenium.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_built_driver()&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Factory &lt;code&gt;(FakeDriver, dispose)&lt;/code&gt;, soit la signature attendue par &lt;code&gt;WebDriversPool&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_pool(max_size=2)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;WebDriversPool[FakeDriver]&lt;/code&gt; prêt à l&amp;rsquo;emploi.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;RecordingPOM(POMBase)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;POM qui consigne ses appels et peut être configuré pour lever.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;acting(pom, step)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ActionSuccess[RecordingPOM]&lt;/code&gt; complet (failure+success en no-op).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;scenario_of(&amp;quot;ok&amp;quot;, &amp;quot;ok2&amp;quot;)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestScenario[FakeDriver]&lt;/code&gt; à N steps.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;failing_scenario(step=&amp;quot;boom&amp;quot;, exc=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénario à 1 step qui lève.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_test(name, scenario=..., test_id=..., skipped=False)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Test[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_suite(name, tests, pool=..., transient_errors=..., max_retries_per_test=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestSuite[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_campaign(name, suites, max_workers=1)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCampaign[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;make_cycle(name=&amp;quot;cycle&amp;quot;, campaigns=..., smoke=..., mode=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestCycle[FakeDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;run_chain(runner)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Helper qui exécute un &lt;code&gt;ChainRunner&lt;/code&gt; et retourne le &lt;code&gt;ActionChain&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;fakedriver&#34;&gt;&lt;code&gt;FakeDriver&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#fakedriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;FakeDriver&lt;/span&gt;:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    title: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;fake&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    disposed: &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;bool&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;C&amp;rsquo;est &lt;strong&gt;tout&lt;/strong&gt;.&#xA;&lt;strong&gt;Le cœur d&amp;rsquo;Ocarina n&amp;rsquo;appelle aucune API Selenium&lt;/strong&gt;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>07.01 — Arborescence</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/01-tree/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/01-tree/</guid>
      <description>&lt;h1 id=&#34;0701arborescence&#34;&gt;07.01 — Arborescence&lt;a class=&#34;anchor&#34; href=&#34;#0701arborescence&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;src/&#xA;├── main.py                                                     # entry point (cf. README)&#xA;│&#xA;├── api/                                                        # clients HTTP externes&#xA;│   ├── retrieve_dashboard_otp_code.py                          # filter + sort + pick OTP&#xA;│   ├── get_otp_history.py                                      # HTTP GET /api/otp-history&#xA;│   └── constants/endpoints.py                                  # URLs des endpoints tests-workers&#xA;│&#xA;├── caches/                                                     # cache L1 in-memory&#xA;│   ├── l1.py                                                   # dogpile.cache.memory TTL 30m&#xA;│   └── reserve_free_cache_key.py                               # UUID + threading.Lock&#xA;│&#xA;├── constants/                                                  # constantes&#xA;│   ├── pages/&#xA;│   │   ├── homepage.py                                         # HOMEPAGE_URL&#xA;│   │   ├── dashboard.py                                        # DASHBOARD_URL + ids des inputs&#xA;│   │   ├── random_loaders.py&#xA;│   │   ├── sacred_upload.py&#xA;│   │   ├── corsicamon.py&#xA;│   │   ├── chaotic_form.py&#xA;│   │   ├── madness.py&#xA;│   │   ├── random_error_page.py&#xA;│   │   └── donkey_sausage_eater_detector.py&#xA;│   └── sys/&#xA;│       ├── redis_keys.py                                       # OTP_SEND_LOCK_KEY, ...&#xA;│       └── transient_errors.py                                 # (WebDriverException, HttpError...)&#xA;│&#xA;├── lib/&#xA;│   ├── connectors/test_steps/actions/                          # fonctions (TPOM) -&amp;gt; TPOM&#xA;│   │   ├── homepage.py&#xA;│   │   ├── dashboard_login.py                                  # ~10 connectors (with/without retries)&#xA;│   │   ├── dashboard_welcome.py&#xA;│   │   ├── dashboard_protected_page.py&#xA;│   │   ├── sacred_upload.py&#xA;│   │   ├── corsicamon_enter_api_key.py&#xA;│   │   ├── corsicamon_main.py&#xA;│   │   ├── chaotic_form.py&#xA;│   │   ├── random_error.py&#xA;│   │   ├── random_loaders.py&#xA;│   │   ├── madness.py&#xA;│   │   ├── this_is_bastia.py&#xA;│   │   ├── cors_errors.py&#xA;│   │   ├── bsod.py&#xA;│   │   ├── dsed.py&#xA;│   │   └── ids_bypassed.py&#xA;│   ├── custom_errors/&#xA;│   │   ├── http.py                                             # HttpErrorPageReachedError&#xA;│   │   └── transient_error.py                                  # TransientError&#xA;│   └── ext/                                                    # extensions / adapters&#xA;│       ├── ocarina/&#xA;│       │   ├── adapters/&#xA;│       │   │   ├── agnostic/&#xA;│       │   │   │   ├── act.py                                  # ⭐ adapter act (hook on_failure)&#xA;│       │   │   │   ├── env_getters.py                          # ⭐ adapter EnvGetters typé&#xA;│       │   │   │   └── match_page.py                           # ⭐ adapter match_page&#xA;│       │   │   └── selenium/&#xA;│       │   │       ├── test_suite.py                           # ⭐ adapter TestSuite&#xA;│       │   │       ├── test_campaign.py                        # ⭐ adapter TestCampaign&#xA;│       │   │       ├── cli_getters.py                          # getters typés depuis le CliStore&#xA;│       │   │       ├── logs.py                                 # create_just_log_error, etc.&#xA;│       │   │       └── screenshotter.py                        # take_screenshot helper&#xA;│       │   └── regex/error_page.py                             # ERROR_PAGE_REGEX&#xA;│       ├── redis/&#xA;│       │   └── client.py                                       # Singleton client Redis&#xA;│       └── selenium/&#xA;│           ├── humanize/                                       # ⭐ HumanizedDriver + keyboard&#xA;│           │   ├── proxy.py&#xA;│           │   └── keyboard.py&#xA;│           ├── pages/verify_elements_presence.py&#xA;│           └── watchers/catch_me_if_you_can_watcher.py         # ⭐ Watcher callback&#xA;│&#xA;├── pages/                                                      # POMs (Selenium + SeleniumTitleMixin)&#xA;│   ├── homepage.py&#xA;│   ├── random_loaders.py&#xA;│   ├── chaotic_form.py&#xA;│   ├── random_error.py&#xA;│   ├── sacred_upload/&#xA;│   │   ├── sacred_upload.py&#xA;│   │   └── fixtures/                                           # fichiers à uploader&#xA;│   ├── dashboard/&#xA;│   │   ├── login.py                                            # ⭐ avec OTP, retries, redis locks&#xA;│   │   ├── welcome_page.py&#xA;│   │   └── protected_page.py&#xA;│   ├── corsicamon/&#xA;│   │   ├── enter_api_key.py&#xA;│   │   └── main.py&#xA;│   ├── madness/&#xA;│   │   ├── base.py&#xA;│   │   ├── matchers.py                                         # has_cors, has_this_is_bastia&#xA;│   │   ├── cors.py&#xA;│   │   └── this_is_bastia.py&#xA;│   └── donkey_sausage_detector/&#xA;│       └── ids_bypassed.py&#xA;│&#xA;└── tests/&#xA;    ├── cycles/e2e.py                                           # ⭐ TestCycle (smoke + main)&#xA;    ├── campaigns/&#xA;    │   ├── global_smoke_tests.py                               # smoke&#xA;    │   ├── corsicamon.py                                       # main (+ smoke variant)&#xA;    │   ├── dashboard_login.py                                  # main&#xA;    │   ├── randomness.py                                       # main&#xA;    │   └── sacred_upload.py                                    # main&#xA;    ├── suites/&#xA;    │   ├── global_smoke_tests.py&#xA;    │   ├── randomness.py&#xA;    │   ├── dashboard/&#xA;    │   │   ├── access/&#xA;    │   │   │   ├── happy_paths.py&#xA;    │   │   │   └── unhappy_paths.py&#xA;    │   │   └── data_driven/multi_login.py&#xA;    │   ├── sacred_upload/{happy_paths,unhappy_paths}.py&#xA;    │   └── corsicamon/{smoke_tests,happy_paths,unhappy_paths}.py&#xA;    └── scenarios/&#xA;        ├── homepage/verify_homepage.py&#xA;        ├── dashboard/&#xA;        │   ├── access/{happy_paths,unhappy_paths}.py&#xA;        │   ├── back_to_igoristan.py&#xA;        │   └── data_driven/{multi_login,datasets/multi_login}.py&#xA;        ├── sacred_upload/{upload_files,just_go_back_to_igoristan}.py&#xA;        ├── corsicamon/{enter_api_key,new_draw,add_corsicamon,back_to_igoristan}.py&#xA;        └── randomness/&#xA;            ├── level_1/{random_error_page,random_loaders_page}.py&#xA;            ├── level_2/{dsed,madness}.py&#xA;            ├── level_3/chaotic_form.py&#xA;            └── level_4/walkthrough.py&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;sémantique-des-dossiers&#34;&gt;Sémantique des dossiers&lt;a class=&#34;anchor&#34; href=&#34;#s%c3%a9mantique-des-dossiers&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Dossier&lt;/th&gt;&#xA;          &lt;th&gt;Rôle&lt;/th&gt;&#xA;          &lt;th&gt;Convention&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;pages/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;POMs (héritent &lt;code&gt;SeleniumTitleMixin, POMBase&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;Une classe par page, fluent (&lt;code&gt;return self&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/connectors/test_steps/actions/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Connectors &lt;code&gt;(TPOM) -&amp;gt; TPOM&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Fonctions pures ; si paramètres, closures&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/custom_errors/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Exceptions projet&lt;/td&gt;&#xA;          &lt;td&gt;Sous-classes d&amp;rsquo;Exception&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/ext/ocarina/adapters/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Adapters au-dessus d&amp;rsquo;Ocarina&lt;/td&gt;&#xA;          &lt;td&gt;Cf. &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/02-adapters/&#34;&gt;&lt;code&gt;02-adapters.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/ext/redis/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Wrapper Singleton Redis&lt;/td&gt;&#xA;          &lt;td&gt;Pour les locks distribués&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;lib/ext/selenium/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Extensions Selenium spécifiques au projet&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;HumanizedDriver&lt;/code&gt;, &lt;code&gt;Watcher&lt;/code&gt; callback&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;caches/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Cache L1 in-memory&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;dogpile.cache.memory&lt;/code&gt; + UUID reservation&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;constants/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Constantes&lt;/td&gt;&#xA;          &lt;td&gt;URLs, redis keys, transient errors&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;api/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Clients HTTP externes&lt;/td&gt;&#xA;          &lt;td&gt;OTP retrieval, OTP history&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tests/&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Hiérarchie ISTQB&lt;/td&gt;&#xA;          &lt;td&gt;cycles / campaigns / suites / scenarios&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;lib-vs-pages&#34;&gt;&lt;code&gt;lib/&lt;/code&gt; vs &lt;code&gt;pages/&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#lib-vs-pages&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;pages/&lt;/code&gt;&lt;/strong&gt; : &lt;em&gt;POMs&lt;/em&gt;, encapsulent l&amp;rsquo;état d&amp;rsquo;une page (locators, méthodes d&amp;rsquo;action).&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;lib/connectors/test_steps/actions/&lt;/code&gt;&lt;/strong&gt; : &lt;em&gt;connectors&lt;/em&gt;, fonctions qui &lt;em&gt;réfèrent&lt;/em&gt; aux POMs.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;act(on_homepage, open_homepage)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;#                ^^^^^^^^^^^^^&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;#                connector défini dans lib/connectors/test_steps/actions/homepage.py&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;open_homepage&lt;/span&gt;(p: Homepage) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; Homepage:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; p&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;open()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le &lt;code&gt;CLAUDE.md&lt;/code&gt; du projet IA en fait une discipline :&lt;/p&gt;</description>
    </item>
    <item>
      <title>03.03 — Évaluation paresseuse</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/03-functional/03-lazy-evaluation/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/03-functional/03-lazy-evaluation/</guid>
      <description>&lt;h1 id=&#34;0303évaluation-paresseuse&#34;&gt;03.03 — Évaluation paresseuse&lt;a class=&#34;anchor&#34; href=&#34;#0303%c3%a9valuation-paresseuse&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Dans Ocarina, &lt;strong&gt;rien n&amp;rsquo;est exécuté tant qu&amp;rsquo;on ne l&amp;rsquo;a pas explicitement déclenché&lt;/strong&gt;. C&amp;rsquo;est ce qui rend les scénarios composables comme des valeurs.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;zzz&#34;&gt;Zzz&lt;a class=&#34;anchor&#34; href=&#34;#zzz&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Endroit&lt;/th&gt;&#xA;          &lt;th&gt;Forme&lt;/th&gt;&#xA;          &lt;th&gt;Déclencheur&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ChainRunner[T]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Thunk[ActionChain[T]]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;runner.run()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;validate(...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;ValidationStartBlock&lt;/code&gt; / &lt;code&gt;ValidationAssertBlock&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.execute()&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;match_page(...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;retourne un &lt;code&gt;ChainRunner[Any]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;.run()&lt;/code&gt; (via la chaîne englobante)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Watcher.callback&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Callable[[Watcher], None]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;_loop&lt;/code&gt; quand &lt;code&gt;start()&lt;/code&gt; est appelé&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;logger.set_prefix(thunk)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Thunk[str]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;recalculé à chaque appel de log&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;Scenario.setup&lt;/code&gt; / &lt;code&gt;teardown&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Effect&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;appelé par &lt;code&gt;TestExecutor&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;bootstrap(post_exec=...)&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Callable[[TestCycleResults], None]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;appelé après &lt;code&gt;run_plugins&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;CliBuilder(effects_factory=lambda ns: (...))&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Effects&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;appelés après le parse argparse&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_scenario: TestScenario[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Callable[[Driver, ILogger], Scenario[Driver]]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;appelé par &lt;code&gt;Test.spawn&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;dispatch[mode]()&lt;/code&gt; dans &lt;code&gt;TestCycle.run_all&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;dict de &lt;code&gt;Thunk[bool]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;appelé en lookup&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;chainrunner&#34;&gt;&lt;code&gt;ChainRunner&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#chainrunner&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;runner &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; drive_page(act1, act2, act3)        &lt;span style=&#34;color:#6272a4&#34;&gt;# ⚠️  rien exécuté&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# … plus tard …&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;chain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; runner&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;run()                         &lt;span style=&#34;color:#6272a4&#34;&gt;# ▶︎  exécution&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Capacité&lt;/th&gt;&#xA;          &lt;th&gt;Sans paresse&lt;/th&gt;&#xA;          &lt;th&gt;Avec paresse&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Stocker un scénario dans une variable&lt;/td&gt;&#xA;          &lt;td&gt;impossible (déjà exécuté)&lt;/td&gt;&#xA;          &lt;td&gt;trivial&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Multiplier &lt;code&gt;[runner] * 5&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;exécute 1 fois, on a 5 références au résultat&lt;/td&gt;&#xA;          &lt;td&gt;exécute 5 fois&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Passer un &lt;code&gt;runner&lt;/code&gt; à un autre &lt;code&gt;runner&lt;/code&gt; (composition)&lt;/td&gt;&#xA;          &lt;td&gt;impossible&lt;/td&gt;&#xA;          &lt;td&gt;trivial&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Réordonner les &lt;code&gt;act&lt;/code&gt; dans un test refactor&lt;/td&gt;&#xA;          &lt;td&gt;difficile&lt;/td&gt;&#xA;          &lt;td&gt;trivial&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;validateexecute&#34;&gt;&lt;code&gt;validate(...).execute()&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#validateexecute&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;v &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; validate(value, name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;x&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_positive)&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;assert_that(is_not_zero)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# … on peut composer …&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;combined &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; chain_validations(v, other_validation)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# … rien d&amp;#39;exécuté jusqu&amp;#39;ici …&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;combined&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;execute()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;raise_if_invalid()        &lt;span style=&#34;color:#6272a4&#34;&gt;# ▶︎  exécution + agrégation&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;C&amp;rsquo;est ce qui permet à &lt;code&gt;_ValidationChain&lt;/code&gt; de &lt;strong&gt;collecter&lt;/strong&gt; toutes les erreurs avant d&amp;rsquo;en lever une seule (&lt;code&gt;AggregateInvariantViolationError&lt;/code&gt;).&lt;/p&gt;</description>
    </item>
    <item>
      <title>07.03 — Scénarios Dashboard login</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/03-scenarios-login/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/03-scenarios-login/</guid>
      <description>&lt;h1 id=&#34;0703scénarios-dashboard-login&#34;&gt;07.03 — Scénarios Dashboard login&lt;a class=&#34;anchor&#34; href=&#34;#0703sc%c3%a9narios-dashboard-login&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Trois familles : happy paths, unhappy paths, data-driven multi-login. Tous exercent le &lt;code&gt;useAuth&lt;/code&gt; à 10% de raté et la coordination OTP.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;dashboard_login&#34;&gt;&lt;code&gt;dashboard_login&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#dashboard_login&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/tests/campaigns/dashboard_login.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_igoristan_login_campaign&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, drivers_pool: SeleniumWebDriversPool) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestCampaign:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; TestCampaign(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Dashboard login&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        suites&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_happy_paths_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_unhappy_paths_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            create_igoristan_login_data_driven_test_suite(drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;happy-paths&#34;&gt;Happy paths&lt;a class=&#34;anchor&#34; href=&#34;#happy-paths&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/tests/suites/dashboard/access/happy_paths.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;create_igoristan_login_happy_paths_test_suite&lt;/span&gt;(&lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;, drivers_pool) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; TestSuite:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; TestSuite(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        name&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Login happy paths&amp;#34;&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        tests&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;[&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_without_otp_happy_path,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_with_otp_happy_path,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            test_dashboard_login_page_back_to_igoristan_button,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ],&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drivers_pool&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;drivers_pool,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;test-1-login-sans-otp&#34;&gt;Test 1 : login sans OTP&lt;a class=&#34;anchor&#34; href=&#34;#test-1-login-sans-otp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;dashboard_login_without_otp_happy_path&lt;/span&gt;(driver, logger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    dashboard_creds &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_env_getters()&lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;get_credentials(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;dashboard&amp;#34;&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_dashboard_login_page &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; DashboardLoginPage(driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    on_dashboard_welcome_page &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; DashboardWelcomePage(driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    just_log_error &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_just_log_error(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    log_error_with_current_url &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_log_error_with_current_url(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger, driver&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;driver)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    just_log_success &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_just_log_success(logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    log_success_with_current_url_and_take_screenshot &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; create_log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    retries_amount &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;max&lt;/span&gt;(get_max_workers(), &lt;span style=&#34;color:#bd93f9&#34;&gt;10&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, open_dashboard_login_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to open the dashboard login page...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(just_log_success(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Opened the dashboard login page!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_dashboard_login_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(log_error_with_current_url(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to verify...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Verified!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                login_without_otp_and_with_retries(dashboard_creds, retries_amount, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to connect to the dashboard without OTP...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(just_log_success(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Connected to the dashboard!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(log_error_with_current_url(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to verify...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Verified!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;retries_amount = max(get_max_workers(), 10)&lt;/code&gt;&lt;/strong&gt; : on rejoue le login interne (au POM) au moins 10 fois, à cause des 10% d&amp;rsquo;échec aléatoire.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;login_without_otp_and_with_retries(creds, retries_amount, logger=logger)&lt;/code&gt;&lt;/strong&gt; : closure qui capture creds + nb retries + logger. Le connector retourne &lt;code&gt;Callable[[DashboardLoginPage], DashboardLoginPage]&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;2 &lt;code&gt;drive_page&lt;/code&gt;&lt;/strong&gt; : login + welcome page. Chacun ouvre, vérifie, screenshot.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Log factories partout&lt;/strong&gt; : aucune lambda inline.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;&lt;code&gt;SeleniumTitleMixin&lt;/code&gt; côté POM&lt;/strong&gt; : &lt;code&gt;get_current_title()&lt;/code&gt; est utilisé par le hook &lt;code&gt;on_failure&lt;/code&gt; de &lt;code&gt;act&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;test-2-login-avec-otp&#34;&gt;Test 2 : login avec OTP&lt;a class=&#34;anchor&#34; href=&#34;#test-2-login-avec-otp&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;dashboard_login_with_otp_happy_path&lt;/span&gt;(driver, logger):&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#6272a4&#34;&gt;# ... setup pareil ...&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    cache &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; in_memory_cache_with_30m_ttl&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fresh_cache_key_for_username &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; reserve_free_cache_key(cache)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fresh_cache_key_for_otp_send_button_click_date &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; reserve_free_cache_key(cache)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, open_dashboard_login_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_dashboard_login_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                start_to_login_with_otp_and_with_retries(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    dashboard_creds, retries_amount,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    cache&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;cache, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    username_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_username,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    otp_send_button_click_date_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_otp_send_button_click_date,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ))&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page, verify_otp_screen)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_login_page,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                type_otp_with_retries(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    retries_amount,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    cache&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;cache, logger&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;logger,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    username_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_username,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    otp_send_button_click_date_cache_key&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;fresh_cache_key_for_otp_send_button_click_date,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ))&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, verify_dashboard_welcome_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_welcome_page, click_on_go_to_nested_page_btn)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            act(on_dashboard_protected_page, verify_dashboard_protected_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Trois &lt;code&gt;drive_page&lt;/code&gt;. Cinq &lt;code&gt;act&lt;/code&gt; dans le premier. Cache L1 + clés réservées pour partager des valeurs entre acts (cf. &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/08-caches-locks/&#34;&gt;&lt;code&gt;08-caches-locks.md&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;</description>
    </item>
    <item>
      <title>03.05 — Programmation déclarative</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/03-functional/05-declarative/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/03-functional/05-declarative/</guid>
      <description>&lt;h1 id=&#34;0305programmation-déclarative&#34;&gt;03.05 — Programmation déclarative&lt;a class=&#34;anchor&#34; href=&#34;#0305programmation-d%c3%a9clarative&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Un scénario d&amp;rsquo;Ocarina &lt;strong&gt;décrit&lt;/strong&gt;, il n&amp;rsquo;exécute pas. C&amp;rsquo;est ce qui le rend factorisable, multipliable, et lisible.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;démonstration&#34;&gt;Démonstration&lt;a class=&#34;anchor&#34; href=&#34;#d%c3%a9monstration&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, open_then_verify_homepage)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to reach the homepage...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;On the homepage!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, click_book_call_page_cta)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to click on the &amp;#39;Book a call&amp;#39; CTA...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(just_log_success(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Clicked on the &amp;#39;Book a call&amp;#39; CTA!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_book_a_call_page, verify_book_call_page)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;failure(just_log_error(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;Failed to verify the &amp;#39;Book a call&amp;#39; page...&amp;#34;&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#ff79c6&#34;&gt;.&lt;/span&gt;success(log_success_with_current_url_and_take_screenshot(&lt;span style=&#34;color:#f1fa8c&#34;&gt;&amp;#34;On the &amp;#39;Book a call&amp;#39; page!&amp;#34;&lt;/span&gt;)),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Ce code &lt;strong&gt;n&amp;rsquo;exécute rien&lt;/strong&gt;. Il &lt;strong&gt;décrit&lt;/strong&gt; :&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapitre 04 — Tests internes du framework</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/</guid>
      <description>&lt;h1 id=&#34;chapitre-04tests-internes-du-framework&#34;&gt;Chapitre 04 — Tests internes du framework&lt;a class=&#34;anchor&#34; href=&#34;#chapitre-04tests-internes-du-framework&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Comment Ocarina se teste lui-même. Cinq familles de tests, une politique de couverture lucide, un rapport Allure historisé sur GitHub Pages.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;Fichier&lt;/th&gt;&#xA;          &lt;th&gt;Sujet&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/01-strategy/&#34;&gt;&lt;code&gt;01-strategy.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Stratégie « &lt;em&gt;dehors comme un utilisateur&lt;/em&gt; » + le &lt;code&gt;conftest.py&lt;/code&gt; (FakeDriver, RecordingPOM, builders).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/02-cram-prysk/&#34;&gt;&lt;code&gt;02-cram-prysk.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Cram tests (&lt;code&gt;prysk&lt;/code&gt;) : fichiers &lt;code&gt;.t&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/03-pytest-scenarios/&#34;&gt;&lt;code&gt;03-pytest-scenarios.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénarios de test appliqués au framework (pytest + allure + hypothesis).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/04-mypy-plugins-types/&#34;&gt;&lt;code&gt;04-mypy-plugins-types.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Tests sur le &lt;strong&gt;typage statiques&lt;/strong&gt; via &lt;code&gt;pytest-mypy-plugins&lt;/code&gt; (&lt;code&gt;*.yml&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/05-syrupy-snapshots/&#34;&gt;&lt;code&gt;05-syrupy-snapshots.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Snapshot tests (&lt;code&gt;syrupy&lt;/code&gt;) pour &lt;code&gt;pretty_print_results&lt;/code&gt; et &lt;code&gt;results_to_json&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/06-hypothesis-properties/&#34;&gt;&lt;code&gt;06-hypothesis-properties.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Property-based testing pour les invariants.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/07-coverage-policy/&#34;&gt;&lt;code&gt;07-coverage-policy.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Politique de couverture : ce qui est testé, ce qui ne l&amp;rsquo;est PAS, pourquoi.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;08&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/04-internal-tests/08-allure-history/&#34;&gt;&lt;code&gt;08-allure-history.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Allure + action composite &lt;code&gt;allure-history&lt;/code&gt; + déploiement GH Pages.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;tableau-récapitulatif&#34;&gt;Tableau récapitulatif&lt;a class=&#34;anchor&#34; href=&#34;#tableau-r%c3%a9capitulatif&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Famille&lt;/th&gt;&#xA;          &lt;th&gt;Outil&lt;/th&gt;&#xA;          &lt;th&gt;Quantité&lt;/th&gt;&#xA;          &lt;th&gt;Sujet&lt;/th&gt;&#xA;          &lt;th&gt;Cible&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Scénarios&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;pytest&lt;/code&gt; + &lt;code&gt;allure-pytest&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;~15 fichiers &lt;code&gt;test_*.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;DSL, orchestration, acteur Playwright&lt;/td&gt;&#xA;          &lt;td&gt;Couvre le &lt;em&gt;comportement&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Cram&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;prysk&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;15 fichiers &lt;code&gt;.t&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;CLI Selenium + Playwright : parsing, validations, defaults&lt;/td&gt;&#xA;          &lt;td&gt;Couvre la &lt;em&gt;surface utilisateur CLI&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Types statiques&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;pytest-mypy-plugins&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;~5 fichiers &lt;code&gt;*test_types.yml&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Inférences de type, narrowing, erreurs attendues&lt;/td&gt;&#xA;          &lt;td&gt;Couvre le &lt;em&gt;typage&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Snapshots&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;syrupy&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;2 fichiers &lt;code&gt;.ambr&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Sortie de &lt;code&gt;pretty_print_results&lt;/code&gt;, &lt;code&gt;results_to_json&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Couvre le &lt;em&gt;format de sortie&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Property-based&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;hypothesis&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;1 fichier (&lt;code&gt;test_invariants_properties.py&lt;/code&gt;)&lt;/td&gt;&#xA;          &lt;td&gt;Comportement sur valeurs aléatoires&lt;/td&gt;&#xA;          &lt;td&gt;Couvre la &lt;em&gt;robustesse aux inputs&lt;/em&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;approche&#34;&gt;Approche&lt;a class=&#34;anchor&#34; href=&#34;#approche&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Dans le fichier &lt;code&gt;conftest.py&lt;/code&gt; :&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.03.06 — drive_page</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/03-railway/06-drive-page/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/03-railway/06-drive-page/</guid>
      <description>&lt;h1 id=&#34;020306drive_page&#34;&gt;02.03.06 — &lt;code&gt;drive_page&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#020306drive_page&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/opinionated/dsl/drive_page.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/opinionated/dsl/drive_page.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;code&#34;&gt;Code&lt;a class=&#34;anchor&#34; href=&#34;#code&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;drive_page&lt;/span&gt;(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    first: ActionSuccess[TPOM], &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;rest: ActionSuccess[TPOM]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;) &lt;span style=&#34;color:#ff79c6&#34;&gt;-&amp;gt;&lt;/span&gt; ChainRunner[TPOM]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; chain_actions(first, &lt;span style=&#34;color:#ff79c6&#34;&gt;*&lt;/span&gt;rest)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;C&amp;rsquo;est &lt;strong&gt;exactement&lt;/strong&gt; &lt;code&gt;chain_actions&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;pourquoi-cet-alias&#34;&gt;Pourquoi cet alias ?&lt;a class=&#34;anchor&#34; href=&#34;#pourquoi-cet-alias&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;h3 id=&#34;1-sémantique-je-prends-le-contrôle-d&#34;&gt;1. Sémantique : « &lt;em&gt;je prends le contrôle d&amp;rsquo;une page&lt;/em&gt; »&lt;a class=&#34;anchor&#34; href=&#34;#1-s%c3%a9mantique-je-prends-le-contr%c3%b4le-d&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Citation du Holy Book (&lt;em&gt;Premiers scénarios&lt;/em&gt;) :&lt;/p&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;&lt;code&gt;drive_page&lt;/code&gt; exprime que l&amp;rsquo;on prend le contrôle d&amp;rsquo;&lt;em&gt;une&lt;/em&gt; page.&#xA;Toute &lt;em&gt;transition&lt;/em&gt; devient explicite par l&amp;rsquo;ouverture d&amp;rsquo;un nouveau &lt;code&gt;drive_page&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;return&lt;/span&gt; [&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, open_homepage)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, verify_homepage)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_homepage, click_cta)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),                                          &lt;span style=&#34;color:#6272a4&#34;&gt;# ⬅ fermeture du contrôle de la homepage&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    drive_page(                                 &lt;span style=&#34;color:#6272a4&#34;&gt;# ⬅ ouverture du contrôle de la page suivante&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        act(on_target_page, verify_target_page)&lt;span style=&#34;color:#ff79c6&#34;&gt;...&lt;/span&gt;,&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ),&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;2-discipline&#34;&gt;2. Discipline&lt;a class=&#34;anchor&#34; href=&#34;#2-discipline&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h3&gt;&#xA;&lt;p&gt;Mélanger des actes sur deux POMs différents dans un même &lt;code&gt;drive_page&lt;/code&gt; devient une erreur &lt;em&gt;mypy&lt;/em&gt; (cf. &lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/03-railway/02-action-chain-states/&#34;&gt;&lt;code&gt;02-action-chain-states.md&lt;/code&gt;&lt;/a&gt;, section « &lt;em&gt;narrowing par &lt;code&gt;act()&lt;/code&gt;&lt;/em&gt; »). Cela force concrètement à respecter la sémantique.&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.06 — Scenario[Driver]</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/06-scenario/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/06-scenario/</guid>
      <description>&lt;h1 id=&#34;0206scenariodriver&#34;&gt;02.06 — &lt;code&gt;Scenario[Driver]&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#0206scenariodriver&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Fichier source : &lt;a href=&#34;https://github.com/mojo-molotov/ocarina/blob/main/src/ocarina/custom_types/scenario.py&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;&gt;&lt;code&gt;src/ocarina/custom_types/scenario.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;dataclass&#34;&gt;Dataclass&lt;a class=&#34;anchor&#34; href=&#34;#dataclass&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@final&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@dataclass(frozen&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;True&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#50fa7b&#34;&gt;Scenario&lt;/span&gt;[Driver]:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    test_chain: TestChain&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    setup: TestSetup &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    teardown: TestTeardown &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    watchers: TestWatchers[Driver] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; field(default&lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6272a4&#34;&gt;# src/ocarina/custom_types/test_components.py&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestChain &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Sequence[ChainRunner[Any]]&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestSetup &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestTeardown &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Effect &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8be9fd;font-style:italic&#34;&gt;type&lt;/span&gt; TestWatchers[Driver] &lt;span style=&#34;color:#ff79c6&#34;&gt;=&lt;/span&gt; Sequence[Watcher[Driver]] &lt;span style=&#34;color:#ff79c6&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ff79c6&#34;&gt;None&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;cycle-de-vie-per-attempt&#34;&gt;Cycle de vie (per attempt)&lt;a class=&#34;anchor&#34; href=&#34;#cycle-de-vie-per-attempt&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;1. setup()           — optionnel, Effect libre (DB, API, …)&#xA;       → lève     :  skip de test_chain, jump à teardown,&#xA;                     return Outcome(setup_failed=True, should_retry=True)&#xA;       → ok       :  continue à test_chain&#xA;&#xA;2. test_chain        — la chaîne réelle (Sequence[ChainRunner])&#xA;       (les watchers tournent pendant ce temps)&#xA;&#xA;3. teardown()        — optionnel, toujours exécuté&#xA;       → lève     :  log warning + ignore (n&amp;#39;affecte pas le verdict)&#xA;&#xA;Si TOUTES les tentatives lèvent au setup :&#xA;    → test marqué SKIPPED (pas FAILED)&#xA;    → log warning « setup keeps failing »&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;setup-et-teardown&#34;&gt;&lt;code&gt;setup&lt;/code&gt; et &lt;code&gt;teardown&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#setup-et-teardown&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;code&gt;setup&lt;/code&gt; et &lt;code&gt;teardown&lt;/code&gt; sont &lt;strong&gt;driver-free et injectionless by design.&lt;/strong&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>09.03.07 — Skills Refactor</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/09-holy-book/03-skills/07-refactor/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/09-holy-book/03-skills/07-refactor/</guid>
      <description>&lt;h1 id=&#34;090307skills-refactor&#34;&gt;09.03.07 — Skills Refactor&lt;a class=&#34;anchor&#34; href=&#34;#090307skills-refactor&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Skills qui &lt;strong&gt;refactor&lt;/strong&gt; la base de tests automatisés existante.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;listing-potentiellement-non-exhaustif&#34;&gt;Listing (potentiellement non exhaustif)&lt;a class=&#34;anchor&#34; href=&#34;#listing-potentiellement-non-exhaustif&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Skill&lt;/th&gt;&#xA;          &lt;th&gt;Cible&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;refactor-fragmentation&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;DRY selon préférence utilisateur&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;introduce-pom-retries&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Retries internes aux POMs, avec dédoublement (first-try + with-retries)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;refactor-fragmentation&#34;&gt;&lt;code&gt;refactor-fragmentation&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#refactor-fragmentation&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;input  : la base de tests&#xA;output : suggestions de refactor DRY :&#xA;            - blocs répétés dans 3+ scénarios → extract en fragment&#xA;            - connectors quasi-identiques → extract un connector paramétré&#xA;            - séquences de log+screenshot répétées → extract un helper&#xA;         + recommandation au cas par cas&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; :&lt;/p&gt;</description>
    </item>
    <item>
      <title>Chapitre 07 — ocarina-example, la suite canonique</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/</guid>
      <description>&lt;h1 id=&#34;chapitre-07ocarina-example&#34;&gt;Chapitre 07 — &lt;code&gt;ocarina-example&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#chapitre-07ocarina-example&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Suite e2e conçue pour tester l&amp;rsquo;Igoristan. C&amp;rsquo;est l&amp;rsquo;&lt;strong&gt;exemple de référence&lt;/strong&gt; : tout projet Ocarina commence par lire ce dépôt et copier ses adapters.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;plan&#34;&gt;Plan&lt;a class=&#34;anchor&#34; href=&#34;#plan&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;#&lt;/th&gt;&#xA;          &lt;th&gt;Fichier&lt;/th&gt;&#xA;          &lt;th&gt;Sujet&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;01&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/01-tree/&#34;&gt;&lt;code&gt;01-tree.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;L&amp;rsquo;arborescence &lt;code&gt;src/&lt;/code&gt; complète : pages/, lib/, api/, caches/, constants/, tests/.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;02&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/02-adapters/&#34;&gt;&lt;code&gt;02-adapters.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Les 5 adapters (&lt;code&gt;act&lt;/code&gt;, &lt;code&gt;match_page&lt;/code&gt;, &lt;code&gt;TestSuite&lt;/code&gt;, &lt;code&gt;TestCampaign&lt;/code&gt;, &lt;code&gt;EnvGetters&lt;/code&gt;).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;03&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/03-scenarios-login/&#34;&gt;&lt;code&gt;03-scenarios-login.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénarios Dashboard login : happy / unhappy / data-driven.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;04&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/04-scenarios-corsicamon/&#34;&gt;&lt;code&gt;04-scenarios-corsicamon.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénarios Corsicamon : api_key, draw, add, back.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;05&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/05-scenarios-randomness/&#34;&gt;&lt;code&gt;05-scenarios-randomness.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénarios randomness (4 levels) : random_error, random_loaders, dsed, madness, chaotic_form, walkthrough.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;06&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/06-scenarios-sacred-upload/&#34;&gt;&lt;code&gt;06-scenarios-sacred-upload.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Scénarios sacred upload : upload_files, back_to_igoristan.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;07&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/07-humanized-driver/&#34;&gt;&lt;code&gt;07-humanized-driver.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;HumanizedDriver&lt;/code&gt; : proxy Selenium pour simuler la saisie humaine.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;08&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/08-caches-locks/&#34;&gt;&lt;code&gt;08-caches-locks.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Cache L1 (&lt;code&gt;dogpile.cache.memory&lt;/code&gt;) + clés réservées + locks Redis distribués.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;09&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/09-watcher-catch-me/&#34;&gt;&lt;code&gt;09-watcher-catch-me.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;Le watcher &lt;code&gt;catch_me_if_you_can&lt;/code&gt; pour le chaotic form.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;10&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/10-env-getters/&#34;&gt;&lt;code&gt;10-env-getters.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;EnvGetters[_CredsKeys, _ValuesKeys]&lt;/code&gt; typé strictement via &lt;code&gt;Literal&lt;/code&gt;.&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;11&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a href=&#34;https://mojo-molotov.github.io/from-ocarina-to-igor/fr/07-ocarina-example/11-ci/&#34;&gt;&lt;code&gt;11-ci.md&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;CI : &lt;code&gt;main_ci.yml&lt;/code&gt; + &lt;code&gt;dev_ci.yml&lt;/code&gt; (lint+typecheck) + &lt;code&gt;e2e.yml&lt;/code&gt; (Redis + Firefox manuel).&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;objectif&#34;&gt;Objectif&lt;a class=&#34;anchor&#34; href=&#34;#objectif&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;p&gt;Citation du Holy Book (chapitre « &lt;em&gt;Premiers pas&lt;/em&gt; ») :&lt;/p&gt;</description>
    </item>
    <item>
      <title>02.12 — Custom types &amp; custom errors</title>
      <link>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/12-custom-types-errors/</link>
      <pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mojo-molotov.github.io/from-ocarina-to-igor/fr/02-ocarina/12-custom-types-errors/</guid>
      <description>&lt;h1 id=&#34;0212custom-types--custom-errors&#34;&gt;02.12 — Custom types &amp;amp; custom errors&lt;a class=&#34;anchor&#34; href=&#34;#0212custom-types--custom-errors&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h1&gt;&#xA;&lt;blockquote class=&#39;book-hint &#39;&gt;&#xA;&lt;p&gt;Couche &lt;em&gt;shape&lt;/em&gt; d&amp;rsquo;Ocarina. Aucune logique : juste des types, des alias, des exceptions, des protocols. C&amp;rsquo;est ce qui rend le DSL typé sans logique runtime additionnelle.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&lt;h2 id=&#34;custom_types&#34;&gt;&lt;code&gt;custom_types/&lt;/code&gt;&lt;a class=&#34;anchor&#34; href=&#34;#custom_types&#34; data-pagefind-ignore=&#34;all&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Fichier&lt;/th&gt;&#xA;          &lt;th&gt;Contenu&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;effect.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type Effect = Callable[[], None]&lt;/code&gt; + &lt;code&gt;type Effects = tuple[Effect, ...]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;thunk.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type Thunk[T] = Callable[[], T]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;tpom.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TypeVar TPOM bound POMBase&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;supports_write.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Protocol SupportsWrite[T]&lt;/code&gt; (&lt;code&gt;write(s: T) -&amp;gt; Any&lt;/code&gt;)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;built_web_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type BuiltWebDriver[Driver] = tuple[Driver, Effect]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;scenario.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;Scenario[Driver]&lt;/code&gt; (frozen dataclass)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_components.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestChain&lt;/code&gt;, &lt;code&gt;TestSetup&lt;/code&gt;, &lt;code&gt;TestTeardown&lt;/code&gt;, &lt;code&gt;TestWatchers[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;test_runner.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestRunner[Driver]&lt;/code&gt; (frozen dataclass)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;oc_test.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestName&lt;/code&gt;, &lt;code&gt;TestScenario[Driver]&lt;/code&gt;, &lt;code&gt;TestScenarioFragment[Driver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;oc_test_layers.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;TestId&lt;/code&gt;, &lt;code&gt;TestResult&lt;/code&gt;, &lt;code&gt;TestSuiteResult&lt;/code&gt;, &lt;code&gt;TestSuiteResults&lt;/code&gt;, &lt;code&gt;TestCampaignResults&lt;/code&gt;, &lt;code&gt;TestCycleResults&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/built_web_driver.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;BuiltSeleniumWebDriver = BuiltWebDriver[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/oc_test_scenario.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;SeleniumTestScenario = TestScenario[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/supported_browsers.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type SupportedSeleniumBrowser = Literal[&amp;quot;chrome&amp;quot;, &amp;quot;firefox&amp;quot;, &amp;quot;edge&amp;quot;, &amp;quot;safari&amp;quot;]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;selenium/web_drivers_pool.py&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;type SeleniumWebDriversPool = WebDriversPool[WebDriver]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Tous ces fichiers sont &lt;em&gt;excluded de la couverture&lt;/em&gt; (&lt;code&gt;pyproject.toml#tool.coverage&lt;/code&gt;) :&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
