Étape 1 de la mission « QA Engineer d’une flotte GenAI multi-tenant ».« Prends en main la plateforme et ton outillage. »
Ce notebook est la couche pédagogique du module 01. Il raconte le module, lit le banc de tests réel (01-decouverte.spec.ts), en orchestre l’inventaire et propose des exercices exécutables. Le fichier .spec.ts reste le moteur de test (backend) : on ne le remplace pas, on l’enrobe.
Portabilité. Toutes les cellules de ce notebook s’exécutent sans navigateur ni instance Open WebUI en ligne : elles lisent le code des tests et en dressent l’inventaire. Lancer réellement les tests E2E (contre une vraie plateforme) est documenté au §4 et fait l’objet du capstone — ici on apprend à lire, cartographier et raisonner sur la suite.
1. Objectifs & place dans la mission
La mission compte cinq étapes. Tu es à la première : avant de certifier quoi que ce soit, il faut prendre en main l’outillage et comprendre comment un test E2E observe une application web réelle.
À la fin de ce module, tu sais :
comprendre l’architecture d’un projet Playwright (config, fixtures, helpers, specs, état authentifié) ;
lire un test E2E et reconnaître ses briques (navigation, localisation, assertion, artefact) ;
choisir des sélecteurs robustes (ID › rôle ARIA › getByRole › classe CSS) ;
distinguer un test réussi / échoué / ignoré et savoir le lancer en mode visible (--headed) pour déboguer.
Étape
Module
Ce que tu certifies
1
01 — Découverte (ici)
Mon outillage fonctionne, je sais lire un test.
2
02 — Navigation & Auth
L’accès et l’admin.
3
03 — Chat & Streaming
Le cœur métier (chat IA).
4
04 — RAG, outils & canaux
Les fonctions avancées.
5
05 — Multi-tenant & CI/CD
L’isolation + l’industrialisation.
# --- Setup : localiser la serie et le module, SANS exposer de chemin absolu ---from pathlib import Pathimport re, shutil, subprocessdef _redact(texte: str) ->str:# Anti-fuite : ne jamais afficher de chemin absolu (machine de l'auteur, home...). out = textefor absolu in {str(Path.cwd().resolve()), str(Path.home())}: out = out.replace(absolu, ".") out = out.replace(absolu.replace("/", "\\"), ".")return outdef trouver_racine_serie(depart=None) -> Path:# La racine de la serie = le dossier qui contient playwright.config.ts. p = Path(depart or Path.cwd()).resolve()for cand in [p, *p.parents]:if (cand /"playwright.config.ts").exists():return candfor sub in Path.cwd().resolve().rglob("playwright.config.ts"):return sub.parentraiseFileNotFoundError("playwright.config.ts introuvable depuis le dossier courant")SERIE = trouver_racine_serie()MODULE = SERIE /"01-decouverte"SPEC = MODULE /"01-decouverte.spec.ts"print("Serie :", SERIE.name)print("Module :", MODULE.name)print("Banc de test:", SPEC.name, "(present)"if SPEC.exists() else"(ABSENT)")
Serie : Playwright-OWUI
Module : 01-decouverte
Banc de test: 01-decouverte.spec.ts (present)
✓ Lecture ancrée — Localisation de la série
La sortie montre que le notebook a identifié la série Playwright-OWUI et localisé le module 01-decouverte avec son banc de test 01-decouverte.spec.ts présent. Cette détection automatisée permet de travailler avec des chemins relatifs sans exposer les chemins absolus de la machine.
2. Pourquoi Playwright, et comment il « voit » la page
Playwright (Microsoft) pilote un vrai navigateur (Chromium/Firefox/WebKit) et exécute des scénarios comme le ferait un humain. Deux idées portent tout le module :
L’auto-wait. Playwright attend automatiquement qu’un élément soit visible/cliquable avant d’agir. On n’écrit (presque) jamais de sleep() : on exprime une attente sur un état (toBeVisible, toHaveText…).
La session réutilisée (storageState). Un setup d’authentification se connecte une fois, sauvegarde cookies/tokens dans .auth/owui.json, et tous les tests repartent déjà connectés — plus rapide et plus robuste face au rate-limiting.
Sélecteurs — du plus robuste au plus fragile :
Rang
Type
Exemple
Pourquoi
1
ID
#chat-input
Stable, unique.
2
Rôle ARIA / label
button[aria-label="…"]
Accessible, résiste au restylage.
3
getByRole / getByText
getByRole('button', { name: /menu/i })
Sémantique, recommandé par Playwright.
4
Classe CSS
.chat-user
Fragile : casse au moindre refactor de style.
Retiens la règle : un sélecteur doit décrire ce que l’élément est (son rôle), pas à quoi il ressemble (sa classe CSS).
# --- Lire le banc de tests et en extraire la structure (sans le lancer) ---texte = SPEC.read_text(encoding="utf-8")# Titres des tests : test('....', ...) — le spec utilise des guillemets simplestitres = re.findall(r"test\('([^']+)'", texte)# Le bloc describe (le theme du module)describe = re.search(r"describe\('([^']+)'", texte)print("Theme du module :", describe.group(1) if describe else"(non trouve)")print()print(f"{len(titres)} test(s) defini(s) dans {SPEC.name} :")for i, t inenumerate(titres, 1):print(f" {i}. {t}")# Compter les exercices embarques (commentaires // EXERCICE)nb_ex =len(re.findall(r"//\s*EXERCICE", texte))print()print(f"{nb_ex} exercice(s) embarque(s) dans les commentaires du spec.")
Theme du module : 01 — Decouverte : Premiers pas avec Playwright
4 test(s) defini(s) dans 01-decouverte.spec.ts :
1. connexion reussie — la page principale se charge
2. le selecteur de modeles liste les modeles disponibles
3. le menu utilisateur est accessible
4. capturer un screenshot de la page principale
4 exercice(s) embarque(s) dans les commentaires du spec.
✓ Lecture ancrée — Analyse du banc de tests
Le spec définit les tests du module : connexion à la page principale, liste des modèles, accès au menu utilisateur, et capture de screenshot. Il contient également des exercices embarqués dans les commentaires, ce qui permet une approche pédagogique progressive.
3. Anatomie d’un test Playwright
Regardons le premier test du module (« connexion réussie »). Sa forme est le squelette de tous les tests E2E :
test.beforeEach(async ({ page }) => {await page.goto('/');// 1. naviguerawaitdismissModals(page);// 2. neutraliser les pop-ups (changelog, etc.)});test('connexion reussie — la page principale se charge',async ({ page }) => {awaitexpect(page.locator(MODEL.selectorButton)) // 3. localiser.toBeVisible({ timeout:15_000 });// 4. asserter (auto-wait)});
Quatre gestes reviennent partout : naviguer → localiser → agir → asserter. Le beforeEach factorise ce qui doit être vrai avant chaque test (ici : être sur la page d’accueil, sans modale qui bloque les clics). Le timeout généreux (15 s) absorbe la lenteur d’une vraie plateforme — ce n’est pas un sleep, c’est une borne sur l’auto-wait.
# --- Inventaire REEL via Playwright : `npx playwright test --grep '01' --list` ---# Liste les tests SANS les executer (--list) et SANS navigateur.# Ne tourne que si npx + node_modules sont presents ; sinon repli propre.def lister_tests_module(serie: Path, grep: str="01"):if shutil.which("npx") isNone:returnNone, "npx introuvable — `npm install` requis (repli : inventaire statique ci-dessus)."ifnot (serie /"node_modules").exists():returnNone, "node_modules absent — `npm install` requis (repli : inventaire statique ci-dessus)."try:# NB: pas de guillemets autour de {grep} — cmd.exe (Windows) les traite# comme litteraux. grep est un token simple (ex. "01"), sans espace. r = subprocess.run(f"npx playwright test --grep {grep} --list", cwd=str(serie), shell=True, capture_output=True, text=True, timeout=180, ) sortie = (r.stdout or"") + (r.stderr or"")return r.returncode, _redact(sortie.strip())exceptExceptionas e:returnNone, f"{type(e).__name__}: {e}"code_retour, sortie = lister_tests_module(SERIE, "01")print("returncode:", code_retour)print(sortie if sortie else"(aucune sortie)")
returncode: 0
Listing tests:
[owui-setup] › fixtures\auth.setup.ts:18:1 › authenticate
[owui] › 01-decouverte\01-decouverte.spec.ts:46:3 › 01 — Decouverte : Premiers pas avec Playwright › connexion reussie — la page principale se charge
[owui] › 01-decouverte\01-decouverte.spec.ts:68:3 › 01 — Decouverte : Premiers pas avec Playwright › le selecteur de modeles liste les modeles disponibles
[owui] › 01-decouverte\01-decouverte.spec.ts:101:3 › 01 — Decouverte : Premiers pas avec Playwright › le menu utilisateur est accessible
[owui] › 01-decouverte\01-decouverte.spec.ts:133:3 › 01 — Decouverte : Premiers pas avec Playwright › capturer un screenshot de la page principale
Total: 5 tests in 2 files
✓ Lecture ancrée — Inventaire Playwright
L’appel npx playwright test --grep 01 --list retourne l’inventaire des tests du module et de la fixture d’authentification, répartis dans les fichiers du banc. Le retour code 0 confirme que la configuration Playwright est valide et que les tests sont bien définis.
4. Lancer le module (pour de vrai)
L’inventaire ci-dessus se fait hors-ligne. Pour exécuter les tests contre une vraie instance Open WebUI, il faut un .env configuré (OWUI_URL, OWUI_EMAIL, OWUI_PASSWORD) — jamais commité — puis :
npm install # dependancesnpx playwright install chromium # navigateurnpm run test:module1 # ce module uniquementnpx playwright test --grep"01"--headed# navigateur VISIBLE (debug)PWDEBUG=1 npx playwright test --grep"01"--headed# Playwright Inspectornpm run report # rapport HTML
L’exécution live dépend d’une plateforme et de secrets : elle est différée ici (et constitue le cœur du capstone). Ce notebook reste reproductible partout, sans secret.
# --- Demonstration executable : classer des selecteurs par robustesse ---# Pas de navigateur : on raisonne sur la *forme* du selecteur (le concept du para. 2).RANGS = {"id": 1, "role": 2, "getby": 3, "css": 4}def robustesse(selecteur: str) ->int: s = selecteur.strip()if s.startswith("#"):return RANGS["id"]if"aria-label"in s or"[role="in s:return RANGS["role"]if s.startswith("getByRole") or s.startswith("getByText"):return RANGS["getby"]if s.startswith(".") or s.startswith("css="):return RANGS["css"]return99# inconnuexemples = ["#chat-input", "button[aria-label='User Menu']","getByRole('button', { name: /menu/i })", ".chat-user"]for s insorted(exemples, key=robustesse):print(f" rang {robustesse(s)} : {s}")
rang 1 : #chat-input
rang 2 : button[aria-label='User Menu']
rang 3 : getByRole('button', { name: /menu/i })
rang 4 : .chat-user
✓ Lecture ancrée — Classement par robustesse
Le classement montre que #chat-input (sélecteur par ID) obtient le rang 1 (le plus robuste), suivi par le sélecteur ARIA button[aria-label='User Menu'] au rang 2. Les sélecteurs par rôle (getByRole) et classe CSS (.chat-user) obtiennent respectivement les rangs 3 et 4, illustrant la hiérarchie de robustesse : ID > ARIA > Rôle > CSS.
5. Interpréter les résultats
Un run renvoie, pour chaque test, un statut. Savoir les distinguer est la base du verdict go/no-go :
passed — le test a vérifié ce qu’il devait. ✅
failed — une assertion a échoué ou un sélecteur n’a rien trouvé → drift d’interface possible, à investiguer.
skipped — volontairement ignoré (service non configuré). Un skip n’est pas une régression.
flaky — passe au retry après un premier échec (souvent : LLM lent, charge). À distinguer d’un vrai échec.
Bilan module 01 (echantillon) : {'passed': 2, 'failed': 1, 'skipped': 1, 'flaky': 0}
Verdict provisoire : NO-GO (investiguer les failed)
✓ Lecture ancrée — Qualification du rapport
Sur l’échantillon de 4 tests, le bilan montre 2 passés, 1 échoué, 1 ignoré, ce qui conduit à un verdict NO-GO nécessitant une investigation des tests en échec. Cette qualification automatisée permet de prendre des décisions rapides sur la stabilité du module.
Exercices
Des exercices, tous exécutables tels quels (ils tournent sans erreur et renvoient un placeholder). Remplace le corps de chaque fonction, ré-exécute, et compare au comportement attendu décrit plus haut.
Exercice 1 — Classer un sélecteur
Complète rang_robustesse pour qu’elle renvoie le rang (1 = ID le plus robuste … 4 = classe CSS la plus fragile) du sélecteur passé, sans réutiliser robustesse du §2. Appuie-toi sur la règle du §2 (rôle vs apparence).
def rang_robustesse(selecteur: str) ->int:# TODO : renvoyer 1 (ID), 2 (role/aria), 3 (getByRole/Text), 4 (classe CSS)# Indice : inspecte le prefixe du selecteur.return-1# placeholder — a remplacer# Test rapide (doit afficher 1 une fois complete) :print("rang de '#chat-input' :", rang_robustesse("#chat-input"))
rang de '#chat-input' : -1
✓ Lecture ancrée — Fonction de robustesse à implémenter
La fonction rang_robustesse retourne actuellement -1 pour le sélecteur #chat-input. Une fois implémentée selon les spécifications (ID=1, ARIA=2, getBy=3, CSS=4), elle retournera 1 pour ce sélecteur par ID.
Exercice 2 — Relier un test à son concept
Complète concept_du_test : à partir du titre d’un test du module, renvoie le concept Playwright principal qu’il illustre (ex. "auto-wait", "count", "getByRole", "screenshot"). Inspire-toi du tableau de la partie pratique du README.
def concept_du_test(titre: str) ->str:# TODO : mapper un titre de test -> son concept principal# Ex : "le selecteur de modeles liste..." -> "count"return"a determiner"# placeholderfor t in ["connexion reussie", "le selecteur de modeles liste les modeles disponibles","le menu utilisateur est accessible", "capturer un screenshot"]:print(f" {t!r} -> {concept_du_test(t)}")
'connexion reussie' -> a determiner
'le selecteur de modeles liste les modeles disponibles' -> a determiner
'le menu utilisateur est accessible' -> a determiner
'capturer un screenshot' -> a determiner
Exercice 3 — L’assertion manquante
Le TEST 1 du spec laisse un exercice en commentaire : ajouter une assertion sur le titre de la page. En TypeScript ce serait await expect(page).toHaveTitle(/Open WebUI/);. Ici, renvoie cette ligne sous forme de chaîne depuis assertion_titre() (on valide la forme, pas l’exécution navigateur).
def assertion_titre() ->str:# TODO : renvoyer la ligne d'assertion Playwright verifiant le titre de page# Forme attendue : await expect(page).toHaveTitle(/Open WebUI/);return"a completer"# placeholderprint(assertion_titre())
a completer
Conclusion & étape suivante
Tu sais désormais lire un test Playwright, cartographier une suite, raisonner sur les sélecteurs et interpréter un bilan — sans avoir eu besoin d’une plateforme en ligne. C’est la fondation : tout le reste de la mission s’appuie dessus.
Note de reproductibilité (C.3). Les cellules de ce notebook ont été exécutées hors-ligne : lecture du .spec.ts, inventaire --list (sans navigateur) et démonstrations pure-Python. Aucune ne requiert d’instance Open WebUI ni de secret. L’exécution live des tests E2E (navigateur + plateforme) est volontairement différée au capstone et sera revalidée en Phase 3 (Open WebUI v0.9.6). Aucun claim de compatibilité v0.9.6 n’est porté par ce module.