import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/modelos/dispositivo_audio.dart'; import 'package:pluriwave/servicios/servicio_dispositivo_audio.dart'; // Type int constants from the platform channel protocol (design doc): // 2 = builtin_speaker, 3 = wired_headset, 8 = bt_a2dp, 14 = usb_headset void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ServicioDispositivoAudioReal', () { const methodChannelName = 'pluriwave/audio_devices'; late ServicioDispositivoAudioReal servicio; setUp(() { servicio = ServicioDispositivoAudioReal(); }); tearDown(() async { await servicio.dispose(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( const MethodChannel(methodChannelName), null, ); }); void stubGetActiveDevice(Map response) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(const MethodChannel(methodChannelName), ( call, ) async { if (call.method == 'getActiveDevice') return response; return null; }); } void stubRequestBluetoothConnect(bool? response) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(const MethodChannel(methodChannelName), ( call, ) async { if (call.method == 'requestBluetoothConnect') return response; return null; }); } test('obtenerDispositivoActual maps builtin_speaker (type=2)', () async { stubGetActiveDevice({ 'id': 'builtin_speaker', 'type': 2, 'name': 'Speaker', }); final device = await servicio.obtenerDispositivoActual(); expect(device.id, 'builtin_speaker'); expect(device.tipo, TipoDispositivo.altavozInterno); expect(device.nombre, 'Speaker'); }); test('obtenerDispositivoActual maps wired_headset (type=3)', () async { stubGetActiveDevice({ 'id': 'wired_headset', 'type': 3, 'name': 'Wired Headset', }); final device = await servicio.obtenerDispositivoActual(); expect(device.id, 'wired_headset'); expect(device.tipo, TipoDispositivo.auricularesCable); }); test('obtenerDispositivoActual maps bt_a2dp (type=8)', () async { stubGetActiveDevice({ 'id': 'bt_a2dp:AA:BB:CC:DD:EE:FF', 'type': 8, 'name': 'My BT Device', }); final device = await servicio.obtenerDispositivoActual(); expect(device.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF'); expect(device.tipo, TipoDispositivo.bluetoothA2dp); }); test('obtenerDispositivoActual maps usb_headset (type=14)', () async { stubGetActiveDevice({ 'id': 'usb_headset:0001:0002', 'type': 14, 'name': 'USB Headset', }); final device = await servicio.obtenerDispositivoActual(); expect(device.id, 'usb_headset:0001:0002'); expect(device.tipo, TipoDispositivo.usbAudio); }); test('obtenerDispositivoActual maps unknown type to desconocido', () async { stubGetActiveDevice({ 'id': 'unknown_device', 'type': 99, 'name': 'Unknown', }); final device = await servicio.obtenerDispositivoActual(); expect(device.tipo, TipoDispositivo.desconocido); }); test('obtenerDispositivoActual updates dispositivoActual cache', () async { stubGetActiveDevice({ 'id': 'builtin_speaker', 'type': 2, 'name': 'Speaker', }); await servicio.obtenerDispositivoActual(); expect(servicio.dispositivoActual?.id, 'builtin_speaker'); }); test('onDispositivoCambiado is a broadcast stream', () { expect(servicio.onDispositivoCambiado.isBroadcast, isTrue); }); // ── bt-device-identity Phase 2: permission contract ────────────────────── test( 'solicitarPermisoBluetooth invokes requestBluetoothConnect and ' 'returns true when granted', () async { stubRequestBluetoothConnect(true); final granted = await servicio.solicitarPermisoBluetooth(); expect(granted, isTrue); }, ); test( 'solicitarPermisoBluetooth returns false when the platform denies', () async { stubRequestBluetoothConnect(false); final granted = await servicio.solicitarPermisoBluetooth(); expect(granted, isFalse); }, ); test( 'solicitarPermisoBluetooth returns false when the platform channel ' 'returns null', () async { stubRequestBluetoothConnect(null); final granted = await servicio.solicitarPermisoBluetooth(); expect(granted, isFalse); }, ); // ── Fix: robust active-device detection (stale green dot) ──────────────── test( 'resubscribir sends cancel+listen to the event channel and forwards ' 'the device the fresh onListen emits', () async { var listens = 0; var cancels = 0; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockStreamHandler( const EventChannel(methodChannelName), MockStreamHandler.inline( onListen: (arguments, events) { listens++; // Mirrors the native onListen immediate resync: every // (re)subscription receives the current active device. events.success({ 'id': 'bt_a2dp:AA:BB:CC:DD:EE:FF', 'type': 8, 'name': 'My BT Device', }); }, onCancel: (arguments) => cancels++, ), ); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockStreamHandler( const EventChannel(methodChannelName), null, ); }); final servicioLocal = ServicioDispositivoAudioReal(); addTearDown(servicioLocal.dispose); // The constructor's initial subscription delivers the first resync. final primero = await servicioLocal.onDispositivoCambiado.first; expect(primero.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF'); expect(listens, 1); expect(cancels, 0); final segundoFuturo = servicioLocal.onDispositivoCambiado.first; await servicioLocal.resubscribir(); final segundo = await segundoFuturo; expect(cancels, 1); expect(listens, 2); expect(segundo.tipo, TipoDispositivo.bluetoothA2dp); expect( servicioLocal.dispositivoActual?.id, 'bt_a2dp:AA:BB:CC:DD:EE:FF', ); }, ); }); }