Autopsie chirurgicale du DOM. Serveur multi-run. Live screencast. Code test prêt.
Un agent IA navigue. Un listener capture. L'IA nettoie. Tu reçois le code — et tu regardes le navigateur travailler en direct dans ton onglet.
Framework-agnostique. CI-ready. Zéro record/replay. Zéro sélecteur fragile.
Les outils existants (Selenium IDE, Katalon Recorder, Cypress Studio) font du record/replay : ils enregistrent ce qu'ils voient, avec les sélecteurs qu'ils trouvent — souvent des XPath absolus ou des classes CSS générées qui cassent au premier refactoring.
DOMAutopsy fait de l'autopsie intelligente : un agent IA navigue comme un humain, un listener JS intercepte chaque interaction et applique une cascade de 7 niveaux pour choisir le sélecteur le plus robuste. L'IA nettoie le bruit. Tu reçois du code prod.
Et depuis la v0.5, c'est un serveur web async multi-run : tu lances N runs en parallèle, tu regardes chacun en live screencast dans ton navigateur, tu intègres tout ça à ton CI/CD via le CLI et le dashboard /ci/{run_id} partageable.
| Selenium IDE | Katalon Recorder | Playwright codegen | DOMAutopsy | |
|---|---|---|---|---|
| Approche | Record/replay | Record/replay | Record/replay | Agent IA + DOM Listener découplés |
| Sélecteurs | XPath absolu / CSS fragile | XPath / CSS | XPath / CSS | Cascade 7 niveaux, validée runtime |
| Shadow DOM | ❌ | ❌ | ✅ natif (chaîne >>>) |
|
| Nettoyage IA | ❌ | ❌ | ❌ | ✅ OpenAI/Groq, filtre bruit + anomalies |
| Redacting credentials | ❌ | ❌ | ❌ | ✅ automatique (sélecteur conservé) |
| 4 formats sortie | ❌ | ❌ Katalon seul | ❌ PW seul | ✅ Katalon / Playwright / Cypress / Selenium |
| Multi-run parallèle | ❌ | ❌ | ❌ | ✅ 50 simultanés, port CDP unique chacun |
| Live screencast web | ❌ | ❌ | ❌ | ✅ WebSocket CDP, 1 capture → N viewers |
| Mode CI + CLI | ❌ | ✅ Bearer token TTL + dashboard partageable | ||
| Replay sans LLM | ❌ | ❌ | ❌ | ✅ qa_player.py Playwright pur |
| Import scripts existants | ❌ | ❌ | ❌ | ✅ parse Katalon/PW/Cy/Sel → NL task |
| Prix | Gratuit | Gratuit | Gratuit | 0€ |
- Python 3.12+
- (optionnel) Caddy 2.x pour le déploiement public/VPN
- Une clé API OpenAI ou Groq
git clone https://github.com/julienmerconsulting/DOMAutopsy
cd DOMAutopsy
pip install -r requirements.txt
playwright install chromium
cp .env.example .env
# édite .env : remplis OPENAI_API_KEY (ou GROQ_API_KEY)python server.py --port 8000
# ouvre http://localhost:8000C'est tout. Le serveur est prêt — UI web, API REST, WebSocket logs+screencast, dashboard CI, le tout sur un seul port.
💡 Mode no-auth par défaut : tant que
DOMAUTOPSY_API_TOKENn'est pas set dans.env, le serveur tourne sans authentification (parfait pour dev local). Active l'auth bearer en une ligne — section Authentification plus bas.
Le listener JS injecté dans Chromium applique une cascade stricte à chaque clic et chaque input. Il choisit le sélecteur le plus robuste disponible, valide son unicité en temps réel via querySelectorAll ou document.evaluate, et remonte si nécessaire.
| Priorité | Stratégie | Exemples |
|---|---|---|
🟢 Tier 1 — data-testid · id · name |
Attributs stables — IDs sémantiques, attributs name, data-testid. Uniques par nature. |
#loginButton · [name="email"] · [data-testid="submit"] |
🔵 Tier 2 — aria-label · placeholder · title |
Attributs sémantiques (accessibilité) — Stables car porteurs de sens métier. Doublon a11y → automatisation, 1 pierre 2 coups. | [aria-label="Rechercher"] · input[placeholder="Email"] |
🩵 Tier 3 — href |
Liens par href — Uniquement si href non générique (#, /, javascript:void) et < 100 chars. |
a[href="/products/catalog"] |
| 🟣 Tier 4 — parent stable | Remontée au parent (icônes, SVG) — Si l'élément cliqué est une icône ou un SVG sans attribut, remonte au parent interactif via composedPath() + closest(). |
[aria-label="Fermer le menu"] |
🟡 Tier 5 — label XPath |
Label associé (inputs) — Pour les inputs sans attribut stable, trouve le <label for> ou <label> wrapping. |
//label[contains(text(),"Mot de passe")]//input |
🔴 Tier 6 — CSS court |
Dernier recours CSS — Max 2 classes stables + nth-of-type. |
button.btn-primary:nth-of-type(2) |
🟤 Tier 7 — XPath texte |
Auto-promotion — Si Tier 1-6 retourne matchCount > 1 mais le texte est court et unique, escalade auto en XPath text-based. |
//button[contains(text(),"Valider")] |
🌑 Shadow DOM — chaîne >>> |
Détection automatique — Construction de la chaîne avec >>> (Playwright) et .shadowRoot.querySelector() (JS). |
my-component >>> [aria-label="Submit"] |
Validation runtime : chaque sélecteur sort de la cascade avec unique: bool + matchCount: N. Pas de devinette : ce qui est marqué unique: true l'est, mesuré dans le DOM réel au moment de la capture.
Bonus sécurité : détection auto des champs sensibles (type="password", autocomplete cc-*/current-password/otp, patterns secret|token|ssn|cvv|pin|api_key sur name|id|aria-label|placeholder|data-testid). Le sélecteur est conservé pour générer un test rejouable, mais la valeur est purgée et marquée sensitive: true. Les credentials ne quittent jamais la machine, ne sont jamais envoyés à l'IA de cleanup, ne figurent jamais dans le rapport HTML.
┌──────────────────┐ HTTP REST + 2 WebSockets ┌──────────────────────────┐
│ Navigateur │ ◄────────────────────────────► │ Serveur FastAPI │
│ (UI web) │ │ server.py — port 8000 │
│ │ /ws/logs/{run_id} │ │
│ ▸ form │ /ws/screen/{run_id} │ Bearer auth (optionnel) │
│ ▸ canvas live │ │ Alloc CDP 9222-9272 │
│ ▸ log box │ │ ScreencastHub (1→N) │
│ ▸ history │ │ Lifespan cleanup │
└──────────────────┘ └──────────────┬───────────┘
│
│ subprocess.Popen
▼
┌─────────────────────────────────────────────────────────┐
│ qa_explorer.py (1 sous-process par run) │
│ │
│ Lance Chromium avec --remote-debugging-port=<unique> │
│ Inject dom_listener.js (3 chemins : add_init_script, │
│ page.evaluate, CDP) │
│ Pilote browser-use (Agent + LLM) │
│ Récupère localStorage.__qaLocatorLog │
│ Dédup → AI cleanup → génère code test → HTML rapport │
│ Écrit tout dans runs/<timestamp>_<run_id>/ │
└──────────────────────────┬──────────────────────────────┘
│ CDP port 9222+offset
▼
┌─────────────────────────────────────────────────────────┐
│ Chromium (1 instance par run) │
│ ▸ dom_listener.js capture click/input/scroll │
│ ▸ browser-use agent décide les actions │
│ ▸ CDP exposé : Page, DOM, (V3 : Network, Console...) │
└─────────────────────────────────────────────────────────┘
📐 Architecture détaillée fichier-par-fichier, schémas séquentiels, pièges techniques : ARCHITECTURE.md (~770 lignes).
Ouvre http://localhost:8000 après python server.py. Tu remplis :
- URL cible
- Task en langage naturel ("Login avec demo/demo, ouvre la facture la plus récente, exporte en PDF")
- Format de sortie (Katalon / Playwright / Cypress / Selenium)
- Provider LLM (OpenAI / Groq)
- Timing avancé (min_wait, max_wait, network_idle) pour les SPA lourdes
- Headless ou visible
Tu cliques Lancer. Un canvas affiche le navigateur en streaming live, une log box scrolle le stdout du subprocess. À la fin : boutons "Voir le rapport HTML" et "Voir le code de test". L'historique des runs est dans la sidebar.
Pour déclencher un run depuis GitHub Actions, Jenkins, ou ton terminal :
# Lance + attend le verdict (exit code 0/1)
python domautopsy_cli.py run \
--server https://dom.example.com \
--url https://app.example.com/login \
--task "Login avec demo/demo, valide" \
--format playwright \
--token $DOMAUTOPSY_TOKEN \
--wait
# Lance + tail le log en direct
python domautopsy_cli.py run --server ... --url ... --task ... --tail
# Suivi d'un run en cours
python domautopsy_cli.py status --server ... --run-id abc123def456Le CLI utilise uniquement urllib stdlib pour le polling REST (zéro dépendance), et fallback proprement si la lib websockets est absente pour --tail.
Tu as déjà une suite Playwright (TypeScript / JS) ? Tu peux la lancer via le serveur, qui stream stdout en WebSocket et écrit un meta.json réutilisable :
python domautopsy_cli.py playwright \
--server https://dom.example.com \
--project-dir /workspace/my-app/tests \
--target login.spec.ts \
--args "--workers 4 --grep happy-path" \
--token $DOMAUTOPSY_TOKEN \
--waitLe serveur appelle npx playwright test dans ton project-dir, force --reporter=list pour un output stream-friendly, et garde la même API /ws/logs/{id} que les runs browser-use. Pas de screencast (Playwright peut spawner N workers, pas de CDP unique).
Tu as un run validé hier ? Tu veux le rejouer aujourd'hui pour vérifier que rien n'a cassé ? /api/replay/{run_id} utilise qa_player.py (Playwright pur, pas d'agent IA) pour rejouer les clean_steps.json du run source :
python domautopsy_cli.py replay \
--server https://dom.example.com \
--run-id abc123def456 \
--waitPas de coût LLM, déterministe, idéal en monitoring de régression.
POST /api/import accepte un fichier .groovy / .ts / .js / .spec.ts / .py. Le serveur parse l'URL initiale + les sélecteurs + les actions, redact automatiquement les valeurs sensibles, et retourne une task en langage naturel suggérée prête à être autofillée dans le form web. Utile pour transformer ta suite legacy en run DOMAutopsy comparable.
Chaque run obtient :
- un
run_id(uuid hex 12 chars) - un port CDP unique dans la plage
9222-9272 - un dossier
runs/<timestamp>_<run_id>/isolé - une queue stdout alimentée par
_pump_stdout(lecture bloquante déléguée au thread pool) - un subprocess.Popen géré par le serveur (kill au shutdown, kill manuel via
DELETE /api/run/{id})
Jusqu'à 50 runs parallèles théoriques (limite = plage de ports CDP).
C'est le détail technique le plus subtil : plusieurs onglets peuvent regarder le même run. Naïvement, chaque viewer ouvrirait sa propre session CDP → N streams Chromium → CPU saturation + bande passante × N.
DOMAutopsy implémente un ScreencastHub par run : 1 seule connexion CDP lit les Page.screencastFrame (jpeg base64) et broadcast vers tous les WebSockets viewers. Quand le dernier viewer se déconnecte, le hub stoppe la capture pour libérer Chromium. Cache de la dernière frame envoyé immédiatement aux nouveaux viewers — pas d'écran noir au join.
3 variables d'env pour tuner :
DOMAUTOPSY_SCREENCAST_EVERY_N=2— 1 frame sur N envoyée (defaut 2 ≈ 15fps si CDP délivre 30)DOMAUTOPSY_SCREENCAST_QUALITY=60— qualité JPEG 0-100DOMAUTOPSY_SCREENCAST_MAX_WIDTH=1280— downscale à la source
Page HTML publique qui s'auto-connecte au log WebSocket d'un run et affiche son verdict. Cas d'usage canonique :
- Ton GitHub Action mint un token TTL → lance un run via CLI → récupère
run_id - Le step suivant met
https://dom.example.com/ci/<run_id>dans le$GITHUB_STEP_SUMMARY - N'importe qui dans l'équipe ouvre l'URL → voit le log live + le verdict final, sans avoir besoin de credentials
Chaque run écrit un meta.json dans son dossier. /api/history scanne runs/, lit chaque meta, trie par date desc. La sidebar UI consomme cet endpoint. Pas de DB pour l'instant (V4 prévoit SQLite pour query rapide).
| Route | Méthode | Auth | Rôle |
|---|---|---|---|
/ |
GET | — | UI principale |
/health |
GET | — | Active runs + auth status (sondes K8s) |
/api/run |
POST | bearer | Lance un run browser-use |
/api/playwright/run |
POST | bearer | Lance npx playwright test |
/api/replay/{run_id} |
POST | bearer | Rejoue un run via qa_player |
/api/import |
POST | bearer | Parse un script existant (multipart) |
/api/run/{id} |
DELETE | bearer | Kill subprocess |
/api/runs |
GET | — | Runs en mémoire |
/api/runs/{id} |
GET | — | Statut complet d'un run (mémoire OU disque) |
/api/history |
GET | — | Runs historiques (limit=N) |
/api/report/{id} |
GET | — | Rapport HTML self-contained |
/api/run/{id}/files |
GET | — | Liste fichiers d'un run |
/api/run/{id}/file/{f} |
GET | — | Sert un fichier (path-traversal-safe) |
/api/formats |
GET | — | Formats de sortie supportés |
/api/providers |
GET | — | Providers LLM + key_present |
/ci/{run_id} |
GET | — | Dashboard CI public |
/ws/logs/{id} |
WS | — | Stream stdout |
/ws/screen/{id} |
WS | — | Stream CDP screencast |
/api/auth/token |
POST | master | Mint un token fils |
/api/auth/tokens |
GET | master | Liste tokens actifs (sans valeurs) |
/api/auth/token/{suffix} |
DELETE | master | Révoque un token fils |
/api/auth/me |
GET | bearer | Whoami |
Modèle simple, deux niveaux :
Configuré côté serveur via DOMAUTOPSY_API_TOKEN dans .env. Immutable, donne tous les droits, sert à minter des tokens fils. Si non set : mode no-auth (parfait pour dev local).
Génération d'un master fort (256 bits d'entropie) :
python domautopsy_cli.py auth genkey --env >> .env
# ou
openssl rand -hex 32
# ou
python -c "import secrets; print(secrets.token_hex(32))"Générés à la volée via le master token, avec un label descriptif + TTL en secondes + scope cosmétique. Stockés en mémoire (perdus au restart serveur — le master continue de marcher). Idéal CI :
# Job de setup en début de pipeline (1 fois)
TOKEN=$(curl -X POST https://dom.example.com/api/auth/token \
-H "Authorization: Bearer $MASTER_TOKEN" \
-d '{"label":"gh-action-run-${{ github.run_id }}","ttl_seconds":7200}' \
| jq -r .token)
# Tous les jobs suivants utilisent $TOKEN
# Il s'auto-détruit dans 2h. Pas besoin de révoquer manuellement.Le CLI a un wrapper plus pratique :
# Récupère juste le token brut, pratique pour eval
export TOKEN=$(python domautopsy_cli.py auth mint \
--token $MASTER_TOKEN --label "ci-job-42" --ttl 7200)
# Liste les tokens actifs
python domautopsy_cli.py auth list --token $MASTER_TOKEN
# Révoque un token par son suffix (8 derniers chars)
python domautopsy_cli.py auth revoke --token $MASTER_TOKEN --suffix abc12345runs/<YYYYMMDD_HHMMSS>_<runid>/
├── locator_log.json # capture brute du listener
├── locator_dedup.json # après dédup (clics consécutifs, dernière valeur input)
├── clean_steps.json # nettoyé par IA + anomalies + code généré
├── test_<format>.<ext> # code rejouable : .groovy / .ts / .js / .py
├── qa_report_<ts>.html # rapport self-contained (Chart.js, dark theme)
└── meta.json # metadata (timestamp, status, counts, verdict)
Self-contained, Chart.js depuis CDN, le reste inline. Contient :
- 5 KPIs : steps nettoyés, actions capturées, sélecteurs uniques, non-uniques, anomalies
- 3 donuts : répartition des stratégies de sélection, fiabilité (unique vs non-unique), types d'actions
- Tableau du parcours nettoyé avec badges de sévérité
- Tableau des locateurs bruts avec stratégie et matchCount par entrée
- Bloc de code généré avec préformaté (mise en évidence visuelle)
- Thème dark cohérent avec la web UI
{
"step": 3,
"action": "click",
"description": "Clique sur le bouton Connexion",
"selector": "[aria-label='Se connecter']",
"selectorType": "css",
"unique": true,
"page": "https://example.com/login"
}import com.kms.katalon.core.testobject.TestObject
import com.kms.katalon.core.testobject.ConditionType
import com.kms.katalon.core.webui.keyword.WebUiBuiltInKeywords as WebUI
// Etape 3 : Clique sur le bouton Connexion
TestObject btnConnexion = new TestObject("btnConnexion")
btnConnexion.addProperty("css", ConditionType.EQUALS, "[aria-label='Se connecter']")
WebUI.verifyElementPresent(btnConnexion, 10)
WebUI.click(btnConnexion)
WebUI.delay(1)DOMAutopsy s'exécute directement avec python server.py --host 0.0.0.0 --port 8000. Pour la prod, Caddy est recommandé en reverse proxy — Caddyfile.example fournit deux modes prêts à coller :
- Mode 1 — public HTTPS avec Let's Encrypt auto, rate limit, security headers (HSTS / CSP / X-Frame-Options...), blocage des paths sensibles, upgrade WebSocket explicite pour
/ws/*(sinon le screencast et le log live meurent silencieusement derrière un proxy mal configuré). - Mode 2 — interne via VPN/Tailscale avec CA locale Caddy (lifetime 30j, auto-renewal), restriction par
remote_ip 127.0.0.1 / VPN range.
# 1. Configure .env
echo "OPENAI_API_KEY=sk-..." >> .env
echo "DOMAUTOPSY_API_TOKEN=$(python domautopsy_cli.py auth genkey)" >> .env
# 2. Lance le serveur Python
python server.py --host 0.0.0.0 --port 8000 &
# 3. Lance Caddy (Let's Encrypt s'occupe du TLS)
sudo caddy run --config Caddyfile
# 4. Vérifie
curl https://domautopsy.example.com/health
# → {"status":"ok", "active_runs":0, "auth_enabled":true, ...}| Variable | Rôle | Défaut |
|---|---|---|
OPENAI_API_KEY |
Clé OpenAI | requis si --provider openai |
GROQ_API_KEY |
Clé Groq | requis si --provider groq |
DOMAUTOPSY_API_TOKEN |
Master Bearer token | vide = no-auth |
DOMAUTOPSY_SCREENCAST_EVERY_N |
1 frame sur N | 2 |
DOMAUTOPSY_SCREENCAST_QUALITY |
Qualité JPEG (0-100) | 60 |
DOMAUTOPSY_SCREENCAST_MAX_WIDTH |
Largeur max px | 1280 |
DOMAUTOPSY_SERVER (CLI client) |
URL serveur défaut | http://localhost:8000 |
| Port | Rôle |
|---|---|
| 8000 | Serveur FastAPI (HTTP + WebSocket) |
| 9222-9272 | Plage CDP Chromium (1 par run actif) |
- Redacting credentials côté listener : détection regex + autocomplete +
type=password, valeur remplacée par<redacted>,sensitive: true, le sélecteur est conservé pour générer un test rejouable (l'utilisateur final injecte sa propre valeur) - Aucun credential ne quitte la machine : pas envoyé à l'IA de cleanup, pas écrit dans
locator_log.json, pas dans le rapport HTML - Path traversal protection sur
/api/run/{id}/file/{filename}: rejette/,\,.. - Subprocess safety :
subprocess.Popenavec args en liste (jamaisshell=True), args validés par Pydantic (RunRequest) - Cleanup au shutdown : lifespan FastAPI tue tous les subprocess actifs (terminate puis kill après 2s)
- Tokens fils in-memory : pas de persistance des credentials secondaires, le master suffit pour les re-mint
- CSP / HSTS / X-Frame-Options : configurés dans
Caddyfile.example - Rate limit Caddy : 500 events/min par IP côté reverse proxy
- Blocage des paths sensibles :
/.git,/.env,/admin,*.py,*.dbcôté Caddy
| Composant | Version min | Rôle |
|---|---|---|
| Python | 3.12 | Runtime principal |
| browser-use | 0.3+ | Agent IA pilotant Chromium |
| playwright | 1.49+ | Driver navigateur + CDP |
| openai | 1.50+ | SDK OpenAI (compat Groq) |
| fastapi | 0.115+ | Serveur HTTP + WebSocket |
| uvicorn[standard] | 0.32+ | ASGI server |
| pydantic | 2.0+ | Validation request bodies |
| aiohttp | 3.10+ | Client CDP WebSocket pour screencast |
| python-dotenv | 1.0+ | Chargement .env |
| python-multipart | 0.0.18+ | Upload /api/import |
| Caddy (recommandé) | 2.x | Reverse proxy HTTPS + WS upgrade |
| Chart.js (CDN) | dernière | Charts dans le rapport HTML |
Fix browser-use CDP frame crash — bug connu (browser-use#2808) : quand une iframe (pub ou cookies) est détruite pendant la navigation, asyncio.gather crashe tout le DOM. DOMAutopsy patche DomService._get_ax_tree_for_all_frames au runtime avec retry + fallback arbre vide.
V1 — consolidation (1 semaine) : alignement API browser-use 0.12, retry+backoff LLM, mode CI --junit-xml, tests unitaires.
V2 — import scripts + diff (3 jours) : drift report contre un script existant, validation "ton test est encore valide".
V3 — observabilité CDP (1 semaine) : capture Runtime.exceptionThrown / Console.messageAdded / Network.* / Coverage.* / Performance.*, audit RGPD 3rd parties, mock generator, anti-flake structurel.
V4 — premium / scale (2 semaines) : grille N×N viewers, persistance SQLite, scheduler intégré, webhooks Slack/Teams, multi-LLM (Anthropic, Gemini, Mistral, DeepSeek), SSO/SAML, export Markdown.
Roadmap détaillée (avec pièges techniques V3 sur le timing de Coverage.startPreciseCoverage, le filtrage Network, et la distinction Browser session vs Page session) : ARCHITECTURE.md §9.
| Outil | Fonction |
|---|---|
| LogLens | Monitoring de logs temps réel |
| DiffLens | Diff visuel sémantique 5 couches |
| DOMAutopsy | Capture DOM + génération code test multi-format |
| QA OPS LAB | SaaS Test Management complet |
Même ADN : Python, zéro dépendance inutile, python server.py, thème dark.
JMer Consulting — JMer Lens Suite — 2026