import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/arranque_audio.dart'; /// S8-android-auto-cold-start: covers [ArranqueAudioApp]'s degraded-path /// bootstrap widget — loading state while pending, swap to the real app on /// completion, and the "wire exactly once" guarantee. void main() { Widget construirArranque({ required Future handlerFuturo, required void Function(String handler) alListo, }) { return ArranqueAudioApp( handlerFuturo: handlerFuturo, alListo: alListo, // Envuelto en su propio MaterialApp — como el `construirApp` real en // main.dart, cuyo resultado (PluriWaveApp) provee su propio // MaterialApp/Directionality; ArranqueAudioApp nunca lo hace por su // cuenta en la rama "lista". construirApp: (handler) => MaterialApp(home: Text('app-lista:$handler')), ); } testWidgets( 'muestra un indicador de carga mientras el future esta pendiente', (tester) async { final completer = Completer(); await tester.pumpWidget( construirArranque(handlerFuturo: completer.future, alListo: (_) {}), ); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); expect(find.textContaining('app-lista'), findsNothing); // Libera el future pendiente para no dejarlo colgado tras el test. completer.complete('handler-x'); await tester.pump(); }, ); testWidgets('cambia a la app real cuando el future se completa', ( tester, ) async { final completer = Completer(); await tester.pumpWidget( construirArranque(handlerFuturo: completer.future, alListo: (_) {}), ); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.complete('handler-y'); await tester.pump(); await tester.pump(); expect(find.text('app-lista:handler-y'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsNothing); }); testWidgets( 'alListo se ejecuta exactamente una vez aunque el widget se reconstruya', (tester) async { final completer = Completer(); final llamadas = []; Widget construir() => construirArranque( handlerFuturo: completer.future, alListo: llamadas.add, ); await tester.pumpWidget(construir()); completer.complete('handler-z'); await tester.pump(); await tester.pump(); // Reconstrucciones adicionales del mismo widget (misma posicion, sin // Key) preservan el State — initState no vuelve a ejecutarse — pero // esto prueba que alListo tampoco se re-dispara desde build(). await tester.pumpWidget(construir()); await tester.pump(); await tester.pumpWidget(construir()); await tester.pump(); expect(llamadas, ['handler-z']); }, ); testWidgets( 'el future falla tras el timeout: sin excepcion no manejada, la app ' 'reemplaza al spinner y alListo nunca se llama', (tester) async { final completer = Completer(); final llamadas = []; await tester.pumpWidget( construirArranque( handlerFuturo: completer.future, alListo: llamadas.add, ), ); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.completeError(Exception('handshake fallido')); await tester.pump(); await tester.pump(); // El error se REPORTA via FlutterError.reportError (por eso // tester.takeException() lo captura) en vez de quedar como una // excepcion async no manejada que tumbe el binding de test/la app. // Consumirlo aqui es la forma de probar "reportado, no crasheado". final excepcionReportada = tester.takeException(); expect(excepcionReportada, isException); expect(excepcionReportada.toString(), contains('handshake fallido')); // El spinner infinito es estrictamente peor que un shell sin handler // conectado — se reemplaza igual, sin texto. expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.textContaining('app-lista'), findsOneWidget); // alListo es solo para el camino exitoso; un future fallido nunca lo // dispara. expect(llamadas, isEmpty); }, ); }