Tous les articles
FlutterQAClaude CodeCI/CD

Mobile QA Autonome avec Claude Code & Ruflo

//
·10 min de lecture

Comment j'ai construit un système de tests mobiles entièrement autonome pour tester une app Flutter sur Android et iOS en parallèle, avec détection de bugs par vision IA, rapports annotés et intégration CI/CD.

Tester une application mobile de A à Z — crashes, UI, accessibilité, performance, résilience réseau — prend des heures en mode manuel. J'ai construit un système autonome qui le fait seul, en 15 à 25 minutes, sur Android et iOS simultanément, sans intervention humaine.

>Le problème

Sur une app Flutter de gestion immobilière, chaque release mobilisait 2 à 3 heures de tests manuels sur deux plateformes. Les régressions passaient au travers. Les bugs d'accessibilité n'étaient jamais détectés. Les rapports étaient informels et non reproductibles.

Il fallait un système reproductible, exhaustif, qui tourne sans intervention humaine et qui produise des rapports exploitables.

>La solution : un swarm de 6 agents IA

Le système repose sur Ruflo v3.5 (orchestrateur multi-agents) et Claude Code (exécution + vision IA). À chaque session, 6 agents spécialisés sont spawned simultanément avec une topologie hiérarchique anti-drift :

  • QA Lead — coordination globale et ordonnancement des phases
  • Android Tester — contrôle l'émulateur Pixel 9 Pro XL via ADB
  • iOS Tester — contrôle le simulateur iPhone 17 via idb
  • Bug Analyst — déduplication et priorisation des bugs
  • UX Reviewer — annotation des screenshots
  • Report Writer — génération des 3 formats de rapport

NOTEAndroid et iOS tournent en parallèle, jamais séquentiellement. Le swarm maintient un contexte partagé en mémoire HNSW pour que chaque agent accède aux données des autres en temps réel.

>Phase 0 — Initialisation du swarm

En un seul message, le swarm est initialisé, le contexte de session stocké en mémoire partagée, et les 6 agents spawned. La règle absolue : tout s'exécute simultanément dans un seul appel.

javascript
// Init swarm hiérarchique
ruflo.swarm_init({
  topology: "hierarchical",
  maxAgents: 6,
  strategy: "specialized",
  name: "mobile-qa-swarm",
});

// Spawn des 6 agents simultanément
ruflo.agent_spawn({ type: "coordinator", name: "QA Lead" });
ruflo.agent_spawn({ type: "tester",      name: "Android Tester" });
ruflo.agent_spawn({ type: "tester",      name: "iOS Tester" });
ruflo.agent_spawn({ type: "analyst",     name: "Bug Analyst" });
ruflo.agent_spawn({ type: "reviewer",    name: "UX Reviewer" });
ruflo.agent_spawn({ type: "architect",   name: "Report Writer" });

>Phase 1 — Démarrage des appareils

Les deux agents testeurs démarrent leurs appareils en parallèle. Si un appareil échoue à booter, l'erreur est capturée en mémoire et la session continue sur la plateforme disponible — aucun blocage.

bash
# Android — attendre le boot complet avant de continuer
adb wait-for-device
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ]; do
  sleep 3
done
adb shell settings put secure accessibility_enabled 1

# iOS — UDID explicite, plus fiable que 'booted'
xcrun simctl boot {DEVICE_UDID}
xcrun simctl bootstatus {DEVICE_UDID} -b
open -a Simulator

⚠ BREAKINGSur iOS 26 bêta, certaines commandes xcrun simctlpeuvent échouer silencieusement. Toujours capturer stderr et stocker l'erreur en mémoire partagée avant de continuer.

>Phase 2 — Analyse statique du code source

Avant même de lancer l'app, Claude Code analyse le code source Flutter avec ses outils natifs (Grep, Glob, Read). L'objectif : détecter les bugs structurels sans exécuter une seule ligne de code.

Patterns recherchés dans l'ensemble du codebase Flutter/Dart :

  • Appels Dio sans try/catch explicite
  • setState() appelé sans vérification if (!mounted) return
  • print() laissés en production (à remplacer par debugPrint)
  • Timers sans .cancel() dans dispose()
  • Formulaires sans validator: sur les TextFormField
  • Strings hardcodées non passées par le système i18n
  • WebSockets non fermés dans dispose()

Le résultat est un JSON structuré avec chaque bug statique référencé par fichier et numéro de ligne, stocké immédiatement en mémoire partagée.

json
{
  "framework": "flutter",
  "static_bugs": [
    {
      "id": "STATIC-001",
      "severity": "high",
      "type": "missing_error_handling",
      "file": "lib/core/services/payment_repository.dart",
      "line": 44,
      "description": "Appel Dio sans gestion d'erreur explicite"
    }
  ],
  "risk_areas": ["paiement", "inspection_signature", "bail_creation"]
}

>Phase 3 — Exploration autonome (boucle vision IA)

C'est le cœur du système. Chaque agent tourne une boucle de 20 actions maximum par plateforme, en 4 étapes répétées jusqu'à épuisement du budget ou action done.

Étape A — Capture d'état

bash
# Android — screenshot + arbre UI filtré
adb exec-out screencap -p > /tmp/screen_android_{N}.png
adb shell uiautomator dump /sdcard/ui.xml
adb pull /sdcard/ui.xml /tmp/ui_android_{N}.xml

# Filtrer à 8 000 caractères max pour contrôler les tokens Claude
python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('/tmp/ui_android_{N}.xml')
nodes = [n for n in tree.iter()
         if n.get('clickable') == 'true' or n.get('focusable') == 'true']
print('\n'.join([ET.tostring(n, encoding='unicode') for n in nodes[:60]])[:8000])
"

# iOS — idb obligatoire (xcrun simctl io ne supporte pas l'arbre UI)
xcrun simctl io {DEVICE_UDID} screenshot /tmp/screen_ios_{N}.png
idb ui describe-all --udid {DEVICE_UDID} 2>/dev/null | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(json.dumps(data.get('elements', [])[:50], indent=2))
"

Étape B — Analyse Claude Vision

Le screenshot et l'arbre UI tronqué sont envoyés à Claude avec un prompt strict. Le modèle retourne un JSON valide avec les bugs détectés et la prochaine action recommandée.

json
{
  "screen_name": "RentalsScreen",
  "bugs": [{
    "id": "ANDROID-007",
    "severity": "high",
    "type": "truncation",
    "title": "Montant FCFA tronqué sur petits écrans",
    "description": "Le montant '1 500 000 FCFA' est coupé à '1 500 0...' dans la liste",
    "reproduction": "Naviguer vers Baux > liste des baux actifs",
    "location": { "x": 320, "y": 445, "element": "ListTile montant" }
  }],
  "next_action": {
    "type": "tap",
    "x": 540,
    "y": 892,
    "reasoning": "Explorer le détail du bail pour vérifier l'affichage complet"
  }
}

NOTELes critères de bug sont définis dans le prompt système : chevauchements UI, contraste insuffisant (<4.5:1), boutons trop petits (<44pt iOS / <48dp Android), labels d'accessibilité manquants, spinners infinis.

Étape C — Exécution de l'action

Sur iOS, xcrun simctl ui ne supporte pas tap/swipe sur Xcode 15+. idb (Facebook iOS Dev Bridge) est indispensable — à démarrer une seule fois avant la session.

bash
# Démarrer idb_companion une seule fois
idb_companion --udid {DEVICE_UDID} --grpc-port 10882 &
idb connect 127.0.0.1 10882

# TAP
adb shell input tap {x} {y}                          # Android
idb ui tap --udid {DEVICE_UDID} {x} {y}              # iOS

# SWIPE
adb shell input swipe {x1} {y1} {x2} {y2} 300        # Android
idb ui swipe --udid {DEVICE_UDID} {x1} {y1} {x2} {y2} # iOS

# SAISIE TEXTE
adb shell input text '{text}'                         # Android
idb ui text --udid {DEVICE_UDID} '{text}'             # iOS

>Phase 4 — Scénarios de test guidés

8 scénarios critiques sont exécutés sur les deux plateformes en parallèle. Le Bug Analyst vérifie chaque assertion par analyse visuelle du screenshot et retourne un résultat JSON structuré.

#ScénarioFocus
1Connexion valide / invalideValidation formulaire, absence d'erreurs 429
2Navigation complète (drawer)Tous les écrans accessibles, aucun crash
3Création d'un contrat (wizard)Formulaire multi-étapes, validation numérique FCFA
4Résilience réseauMode avion → reprise WebSocket automatique
5Switch de rôleIsolation des données entre espaces utilisateur
6Accessibilité + grande policeTalkBack / VoiceOver, texte au maximum
7Changement de langue FR ↔ ENAucune chaîne non traduite acceptable
8Dashboard sans rate limiting3 allers-retours en &lt; 30 s, aucun HTTP 429

>Phase 5 — Déduplication des bugs

Le Bug Analyst récupère tous les bugs depuis la mémoire partagée et applique ces règles dans l'ordre :

  • Même bug Android + iOS → un seul bug avec platforms: ["android", "ios"]
  • Bug cross-platform → sévérité escaladée d'un niveau (medium → high, high → critical)
  • Même type sur plusieurs écrans → regroupé avec affected_screens: [...]
  • Bug statique confirmé dynamiquement → entrées fusionnées

>Phase 6 — Annotation des screenshots

Chaque screenshot de bug est annoté automatiquement avec un rectangle rouge et un badge ID via Pillow (Python), puis sauvegardé dansqa-reports/screenshots/.

python
from PIL import Image, ImageDraw

def annotate_screenshot(img_path: str, bug: dict) -> str:
    img = Image.open(img_path)
    draw = ImageDraw.Draw(img)
    x, y = bug["location"]["x"], bug["location"]["y"]

    draw.rectangle([x - 5, y - 5, x + 120, y + 50], outline="red", width=3)
    draw.rectangle([x - 5, y - 28, x + 90, y - 5], fill="red")
    draw.text((x + 2, y - 24), bug["id"], fill="white")

    output = img_path.replace(".png", f"_bug_{bug['id']}.png")
    img.save(output, format="PNG")
    return output

>Phase 7 — Rapports en 3 formats

  • rapport-qa.md — Markdown lisible pour l'équipe et les PR comments GitHub
  • rapport-qa.json — JSON structuré pour les pipelines CI/CD et dashboards
  • alerte-slack.json — Payload webhook Slack envoyé automatiquement si des bugs critiques sont détectés
bash
# Envoi automatique si bugs critiques détectés
curl -X POST -H 'Content-type: application/json' \
  --data @./qa-reports/alerte-slack.json \
  "$SLACK_WEBHOOK_URL"

>Intégration CI/CD — GitHub Actions

Le prompt peut être lancé automatiquement à chaque PR ou sur cron, sans émulateur physique — l'émulateur Android est géré parreactivecircus/android-emulator-runner.

yaml
name: Mobile QA Autonome
on:
  pull_request:
    branches: [main, develop]
  schedule:
    - cron: '0 2 * * 1-5'   # Lundi–vendredi à 2h

jobs:
  qa-audit:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start Android Emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 35
          arch: x86_64

      - name: Run QA Agent
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          claude --dangerously-skip-permissions \
            "Exécute le plan QA mobile. App : ${{ vars.APP_BUNDLE_ID }}.
             Rapports dans ./qa-reports/."

      - name: Post report on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const report = require('fs').readFileSync('./qa-reports/rapport-qa.md', 'utf8')
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: report
            })

>Optimisation des coûts API

  • Triage Haiku — premier OUI/NON sur chaque screenshot avec claude-haiku-4-5-20251001, puis Sonnet uniquement si un bug est probable. Économie ~60%.
  • Prompt caching — le system prompt est identique pour tous les appels d'analyse. Ajouter cache_control: ephemeral sur le system block. Économie ~40%.
  • Budget réduit — 20 actions au lieu de 50 pour les sessions de régression rapide avant release.
ModeDuréeCoût API
Audit complet (Android + iOS)15–25 min$0.40–$0.80
Android uniquement8–12 min$0.15–$0.35
Flow spécifique4–7 min$0.05–$0.15
Régression rapide6–10 min$0.10–$0.25

>Résultats observés

Sur 5 sessions de test, le système a réduit le temps de QA de 2 à 3 heures à 15–25 minutes, avec une couverture supérieure. Les bugs les plus fréquemment détectés automatiquement :

  • Montants monétaires tronqués dans les listes (formatage spécifique à la locale)
  • Spinners infinis sans timeout sur les endpoints lents
  • Labels d'accessibilité manquants sur les FAB et icônes de navigation
  • Confusion de navigation dans les structures de drawer complexes (40+ écrans)
  • Bugs cross-platform détectés uniquement par la déduplication automatique

Récupérer le prompt complet

Le prompt et le guide README sont disponibles directement — copiez-les ou téléchargez-les pour les utiliser dans Claude Code.

>Stack technique

  • Ruflo v3.5 — orchestration swarm, mémoire partagée HNSW, hooks intelligents
  • Claude Code + claude-sonnet-4-6 — exécution des agents et vision IA
  • ADB — contrôle émulateur Android (Pixel 9 Pro XL)
  • idb / idb_companion (Facebook) — contrôle simulateur iOS (xcrun simctl ui ne supporte pas tap/swipe sur Xcode 15+)
  • Pillow (Python) — annotation automatique des screenshots
  • Flutter / Dart — framework de l'application cible