import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/servicio_audio.dart'; import 'package:pluriwave/widgets/visualizador_audio.dart'; /// Play sensitive-permission guard for the waveform visualizer. /// /// `MainActivity.startVisualizerWhenAllowed` asks for `RECORD_AUDIO` the /// instant Dart subscribes to the `pluriwave/audio_visualizer` EventChannel. /// The home screen's "Escuchar" hero mounts this widget, so before this the /// microphone dialog appeared on the user's FIRST play, with no context at /// all, in a radio app — the worst possible moment to ask. /// /// The subscription (and therefore the permission request) is now gated on /// an explicit opt-in the user gives in Settings, after reading what the /// permission is for. These tests pin the Dart half of that: no opt-in, no /// channel subscription. void main() { const canal = MethodChannel('pluriwave/audio_visualizer'); late List llamadas; setUp(() { llamadas = []; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(canal, (call) async { llamadas.add(call); return null; }); }); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(canal, null); }); Future montar( WidgetTester tester, { required Stream estadoStream, required Stream sessionStream, required bool capturaRealHabilitada, }) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: VisualizadorAudio( estadoStream: estadoStream, androidAudioSessionIdStream: sessionStream, barras: 12, capturaRealHabilitada: capturaRealHabilitada, ), ), ), ); await tester.pump(); } testWidgets( 'sin el opt-in, reproducir NO suscribe el EventChannel (y por tanto no ' 'dispara la peticion de RECORD_AUDIO)', (tester) async { final estado = StreamController.broadcast(); final sesion = StreamController.broadcast(); addTearDown(estado.close); addTearDown(sesion.close); await montar( tester, estadoStream: estado.stream, sessionStream: sesion.stream, capturaRealHabilitada: false, ); sesion.add(42); estado.add(EstadoReproduccion.reproduciendo); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect( llamadas.where((c) => c.method == 'listen'), isEmpty, reason: 'subscribing is what makes MainActivity call requestPermissions ' 'for RECORD_AUDIO; with no opt-in it must never happen', ); }, ); testWidgets( 'con el opt-in, reproducir SI suscribe el EventChannel (la onda real ' 'sigue disponible para quien la pide)', (tester) async { final estado = StreamController.broadcast(); final sesion = StreamController.broadcast(); addTearDown(estado.close); addTearDown(sesion.close); await montar( tester, estadoStream: estado.stream, sessionStream: sesion.stream, capturaRealHabilitada: true, ); sesion.add(42); estado.add(EstadoReproduccion.reproduciendo); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(llamadas.where((c) => c.method == 'listen'), hasLength(1)); }, ); testWidgets('el visualizador sigue dibujando sus barras sin el opt-in', ( tester, ) async { final estado = StreamController.broadcast(); final sesion = StreamController.broadcast(); addTearDown(estado.close); addTearDown(sesion.close); await tester.pumpWidget( MaterialApp( home: Scaffold( body: VisualizadorAudio( estadoStream: estado.stream, androidAudioSessionIdStream: sesion.stream, barras: 12, barrasDiscretas: true, ), ), ), ); await tester.pump(); for (var i = 0; i < 12; i++) { expect(find.byKey(Key('visualizador-barra-$i')), findsOneWidget); } }); }