e2e-test

Par divinevideo · divine-mobile

Exécuter et déboguer des tests d'intégration E2E contre la stack Docker locale. Couvre la configuration de l'émulateur, la gestion de la stack, l'exécution des tests, la capture des logs et les patterns d'échec courants. À utiliser lors de l'exécution de tests E2E, du débogage d'échecs de tests ou de la mise en place de l'infrastructure de test locale.

npx skills add https://github.com/divinevideo/divine-mobile --skill e2e-test

Tests d'intégration E2E

Les tests end-to-end exécutent l'application complète contre une stack Docker locale. Ils exercent des flux OAuth réels, des souscriptions relay et des uploads de médias sans aucun mock.

Démarrage rapide

cd mobile/
mise run local_up        # Démarrer la stack Docker
mise run e2e_test        # Exécuter tous les tests E2E avec profilage
mise run e2e_test integration_test/auth/auth_journey_test.dart  # Test unique
mise run local_down      # Arrêter la stack Docker
mise run local_reset     # Effacer les données et redémarrer
mise run local_status    # Vérifier la santé des services

Toujours utiliser mise run e2e_test — il gère local_up, le profilage des logs, et les rapports de timeline fusionnés. Ne jamais appeler patrol test directement.

Stack Docker locale

Situé dans local_stack/. Services :

Service Port hôte Objectif
Keycast 43000 OAuth + signer NIP-46
FunnelCake Relay 47777 Relay Nostr (WebSocket)
FunnelCake API 43001 API REST
Blossom 43003 Serveur média
Postgres 15432 Base de données Keycast

Toutes les images GHCR sont publiques — aucune connexion Docker requise. Si les pulls échouent avec denied, vérifiez si vous avez une session docker login ghcr.io obsolète. Un token expiré fait que Docker envoie de mauvaises credentials au lieu de revenir à l'accès anonyme. Solution : docker logout ghcr.io.

Stack ne démarre pas

Si mise run local_up échoue lors du pull d'images :

# Utiliser les images en cache, ignorer le pulling
COMPOSE_FILE=../local_stack/docker-compose.yml docker compose up -d --pull=missing

Puis exécutez le script de test directement :

bash ../local_stack/profile.sh integration_test/auth/

Émulateur Android

Configuration

L'émulateur atteint l'hôte via 10.0.2.2. Les constantes de port sont dans lib/models/environment_config.dart et ré-exportées par integration_test/helpers/constants.dart.

Lancement Linux / Hyprland

DISPLAY=:1 ANDROID_AVD_HOME=/home/daniel/.config/.android/avd \
  emulator -avd Medium_Phone_API_36.1 -gpu host -no-snapshot-load

Toujours utiliser -gpu host pour le rendu vidéo (swiftshader ne peut pas rendre les frames media_kit).

Problèmes de stockage

Les installations répétées d'APK remplissent /data. Symptômes : INSTALL_FAILED_INSUFFICIENT_STORAGE ou 0 tests découverts avec code de sortie Gradle 1.

# Vérifier l'espace
adb shell df -h /data

# Libérer de l'espace
adb shell pm trim-caches 1G

# Option nucléaire : effacer l'émulateur
emulator -avd <name> -gpu host -wipe-data

Buffer Logcat

La valeur par défaut 256KB est trop petite — les logs du flux auth tournent avant les phases critiques.

adb logcat -G 16M          # Augmenter à 16MB
adb logcat -c              # Effacer avant l'exécution du test
adb logcat -d | grep 'flutter.*\[AUTH\]'  # Capturer le flux auth

Filtrer par PID pour isoler les cas de test (chaque patrolTest s'exécute dans un nouveau processus) :

adb logcat -d | grep '<PID>.*flutter.*\[AUTH\]' | grep -v 'Router redirect'

Framework de test

Patrol

Les tests utilisent Patrol pour l'automatisation native de l'UI. Patrol encapsule integration_test de Flutter avec la capacité de gérer les dialogues de permissions, le bouton retour système, les notifications et les feuilles de partage.

patrolTest('my test', ($) async {
  final tester = $.tester;
  // Utiliser tester pour les interactions de widget Flutter
  // Utiliser $ pour les interactions natives (permissions, UI système)
});

Bundling de tests Patrol (faux positifs)

Patrol regroupe TOUS les fichiers de test d'un répertoire dans un seul APK. Chaque fichier de test s'exécute dans un processus d'instrumentation séparé. Quand le test B s'exécute, le code du test A est dans le bundle mais « non demandé » — Patrol le marque [E].

Ce sont des faux positifs, pas des vrais échecs. Fiez-vous uniquement aux lignes de statut final / du test runner. Le logcat affichera :

registered test "foo_test ..." was not matched by requested test "bar_test ..."

Motif de lancement d'app

Utiliser launchAppGuarded depuis test_setup.dart pour capturer les erreurs async relay :

final originalOnError = suppressSetStateErrors();
final originalErrorBuilder = saveErrorWidgetBuilder();

launchAppGuarded(app.main);
await tester.pumpAndSettle(const Duration(seconds: 3));

// ... corps du test ...

restoreErrorWidgetBuilder(originalErrorBuilder);
restoreErrorHandler(originalOnError);
drainAsyncErrors(tester);

Polling au lieu de pumpAndSettle

L'app a des timers de polling persistants qui empêchent pumpAndSettle de se stabiliser. Utiliser des boucles pump manuelles :

for (var i = 0; i < 60; i++) {
  await tester.pump(const Duration(milliseconds: 250));
  if (find.text('Welcome').evaluate().isNotEmpty) break;
}

Pièges courants

Publication asynchrone → requête Relay

La publication vidéo est asynchrone — l'UI navigue vers le profil avant que l'upload blossom et la publication relay se terminent. Toujours interroger le relay :

var events = <Event>[];
for (var i = 0; i < 120; i++) {
  await tester.pump(const Duration(milliseconds: 500));
  events = await queryRelay(filter);
  if (events.isNotEmpty) break;
}

Feuilles d'onboarding bloquant l'UI

Les nouvelles fonctionnalités peuvent ajouter des bottom sheets qui couvrent les boutons dont le test a besoin. Les fermer avant de continuer :

for (var i = 0; i < 20; i++) {
  await tester.pump(const Duration(milliseconds: 250));
  final gotIt = find.text('Got it!');
  if (gotIt.evaluate().isNotEmpty) {
    await tester.tap(gotIt);
    await tester.pump(const Duration(milliseconds: 500));
    break;
  }
}

Crashes d'initialisation de provider Riverpod

Si un provider utilise requireIdentity ou des getters non-nullable similaires, il crash au démarrage froid (avant l'auth) et Riverpod met en cache l'erreur définitivement. Utiliser l'accès nullable (currentIdentity) dans les providers et gérer null. L'erreur ressemble à :

ProviderException: Tried to use a provider that is in error state.
Bad state: requireIdentity called with no active NostrIdentity.

Ancêtre Material Widget

TextField requiert un ancêtre Material. Si un widget est utilisé dans un contexte overlay ou transition sans Scaffold, le wrapper :

Material(
  color: Colors.transparent,
  child: TextField(...),
)

Helpers de test

Tous les helpers sont dans integration_test/helpers/ :

Fichier Objectif
constants.dart Constantes de port + pgPort, appPackage
db_helpers.dart Requêtes Postgres : jetons de vérification, jetons de rafraîchissement
http_helpers.dart API Keycast : vérifier email, mot de passe oublié
navigation_helpers.dart Interactions UI : registre, connexion, appuyer sur onglets, attendre les widgets
relay_helpers.dart Publier des événements Nostr : kind 34236 vidéos, kind 0 profils
test_setup.dart Suppression d'erreurs, lancement d'app, drainages d'erreurs async

Débogage

# Logs des services
docker compose -f local_stack/docker-compose.yml logs keycast --tail=50
docker compose -f local_stack/docker-compose.yml logs funnelcake-relay --tail=50
docker compose -f local_stack/docker-compose.yml logs blossom --tail=50

# Vérifier blossom pour les uploads actuels (pas seulement les health checks)
docker compose -f local_stack/docker-compose.yml logs blossom | grep -v 'path=/'

# Trace du flux auth
adb logcat -d | grep 'flutter.*\[AUTH\]' | grep -v 'Router redirect'

# Rapports de test (timeline fusionnée Docker + logcat)
ls mobile/test_reports/*.jsonl

Skills similaires