import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/arranque_audio.dart'; /// S8-android-auto-cold-start: covers [esperarArranqueAudio]'s timeout race /// in isolation, without touching the real `audio_service` plugin (Design /// "Testability" — the init future is injected). void main() { group('esperarArranqueAudio', () { test( 'handler resuelto antes del timeout devuelve ArranqueAudioListo', () async { final resultado = await esperarArranqueAudio( Future.value('handler-ok'), timeout: const Duration(milliseconds: 200), ); expect(resultado, isA>()); expect((resultado as ArranqueAudioListo).handler, 'handler-ok'); }, ); test('handler colgado mas alla del timeout devuelve ArranqueAudioPendiente ' 'con el MISMO future original', () async { final completer = Completer(); final resultado = await esperarArranqueAudio( completer.future, timeout: const Duration(milliseconds: 20), ); expect(resultado, isA>()); expect( identical( (resultado as ArranqueAudioPendiente).handlerFuturo, completer.future, ), isTrue, reason: 'init nunca debe llamarse dos veces — el future pendiente ' 'debe ser exactamente el mismo objeto', ); // El completer nunca se completó — liberarlo evita un future // colgado tras el test. completer.complete('handler-tardio'); }); test('finalizacion tardia tras el timeout resuelve el future original con ' 'el handler real', () async { final completer = Completer(); final resultado = await esperarArranqueAudio( completer.future, timeout: const Duration(milliseconds: 20), ); final pendiente = resultado as ArranqueAudioPendiente; completer.complete('handler-tardio'); final handler = await pendiente.handlerFuturo; expect(handler, 'handler-tardio'); }); }); /// fix/notificacion-media — commit 1: `AudioService.asyncError` had zero /// subscribers, so every exception `audio_service` swallows internally was /// dropped on the floor. These cover the injectable seam only (Design /// "Testability" — the stream and the logger are both injected), never the /// real plugin. group('observarErroresAudio', () { test('reenvia al logger cada error emitido, en orden', () async { final controlador = StreamController.broadcast(); final registrados = []; final sub = observarErroresAudio( controlador.stream, registrar: registrados.add, ); controlador.add('fallo-1'); controlador.add(StateError('fallo-2')); await controlador.close(); expect(registrados, hasLength(2)); expect(registrados.first, 'fallo-1'); expect(registrados.last, isA()); await sub.cancel(); }); test('cancelar la suscripcion corta el logging — no puede filtrarse ' 'tras el teardown del handler', () async { final controlador = StreamController.broadcast(); final registrados = []; final sub = observarErroresAudio( controlador.stream, registrar: registrados.add, ); controlador.add('antes-del-cancel'); // Deja que el evento se entregue antes de cancelar (los broadcast // controllers entregan en un microtask, no de forma sincrona). await Future.delayed(Duration.zero); await sub.cancel(); controlador.add('despues-del-cancel'); await controlador.close(); expect( registrados, ['antes-del-cancel'], reason: 'tras cancelar, la suscripcion no debe seguir viva ni registrar ' 'nada mas', ); }); test( 'un evento de error del propio stream tambien llega al logger', () async { final controlador = StreamController.broadcast(); final registrados = []; final sub = observarErroresAudio( controlador.stream, registrar: registrados.add, ); // Rama defensiva: el plugin solo usa `add`, nunca `addError`, pero un // error de stream sin manejar seria una excepcion no capturada. controlador.addError(const FormatException('stream roto')); await controlador.close(); expect(registrados, hasLength(1)); expect(registrados.single, isA()); await sub.cancel(); }, ); test('el logger por defecto acepta cualquier objeto sin lanzar', () { expect( () => registrarErrorAudioService(StateError('cualquier cosa')), returnsNormally, ); expect( () => registrarErrorAudioService('un string suelto'), returnsNormally, ); }); }); }