import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/mpv/mpv.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/video_volume_controller.dart'; import 'package:plezy/widgets/video_controls/widgets/volume_control.dart'; import '../test_helpers/prefs.dart'; void main() { setUp(() async { resetSharedPreferencesForTest(); SettingsService.resetForTesting(); await SettingsService.getInstance(); LocaleSettings.setLocaleSync(AppLocale.en); }); testWidgets('mute button keeps and restores the exact non-zero volume', (tester) async { final settings = SettingsService.instance; await settings.write(SettingsService.volume, 37.0); final player = _VolumePlayer(37); final controller = VideoVolumeController(player: player, settings: settings, initialVolume: 37); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold(body: VolumeControl(volumeController: controller)), ), ); await tester.tap(find.byType(IconButton)); await controller.idle; await tester.pump(); expect(player.volume, 0); expect(settings.read(SettingsService.volume), 37); await tester.tap(find.byType(IconButton)); await controller.idle; await tester.pump(); expect(player.volume, 37); expect(settings.read(SettingsService.volume), 37); expect(player.volumeChanges, [0, 37]); }); testWidgets('D-pad repeats accumulate before native volume publication', (tester) async { final settings = SettingsService.instance; await settings.write(SettingsService.volume, 50.0); final player = _DelayedVolumePlayer(50); final controller = VideoVolumeController(player: player, settings: settings, initialVolume: 50); final focusNode = FocusNode(); addTearDown(() async { controller.dispose(); focusNode.dispose(); await player.close(); }); await tester.pumpWidget( MaterialApp( home: Scaffold( body: VolumeControl(volumeController: controller, focusNode: focusNode), ), ), ); focusNode.requestFocus(); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.select); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.arrowRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight); await tester.pump(); expect(controller.value, 65); expect(tester.widget(find.byType(Slider)).value, 65); expect(player.volumeChanges, [55]); player.completeNext(); await tester.pump(); expect(player.volumeChanges, [55, 65]); player.completeNext(); await controller.idle; await tester.pump(); expect(player.volume, 65); expect(settings.read(SettingsService.volume), 65); }); testWidgets('slider previews are serialized and only the final drag value persists', (tester) async { final settings = SettingsService.instance; await settings.write(SettingsService.volume, 50.0); final player = _VolumePlayer(50); final controller = VideoVolumeController(player: player, settings: settings, initialVolume: 50); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold(body: VolumeControl(volumeController: controller)), ), ); await tester.drag(find.byType(Slider), const Offset(40, 0)); await controller.idle; await tester.pump(); expect(player.volumeChanges, isNotEmpty); expect(settings.read(SettingsService.volume), closeTo(controller.value, 0.001)); }); testWidgets('one ancestor wheel delta applies over and away from the slider', (tester) async { final settings = SettingsService.instance; await settings.write(SettingsService.volume, 50.0); final player = _VolumePlayer(50); final controller = VideoVolumeController(player: player, settings: settings, initialVolume: 50); addTearDown(controller.dispose); const blankKey = Key('blank-player-area'); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Listener( behavior: HitTestBehavior.translucent, onPointerSignal: (event) { if (event is PointerScrollEvent) { controller.adjust(-event.scrollDelta.dy / 20); } }, child: Row( children: [ const SizedBox(key: blankKey, width: 100, height: 100), VolumeControl(volumeController: controller), ], ), ), ), ), ); await tester.sendEventToBinding( PointerScrollEvent(position: tester.getCenter(find.byType(Slider)), scrollDelta: const Offset(0, -100)), ); await controller.idle; expect(controller.value, 55); expect(player.volumeChanges, [55]); await tester.sendEventToBinding( PointerScrollEvent(position: tester.getCenter(find.byKey(blankKey)), scrollDelta: const Offset(0, -100)), ); await controller.idle; expect(controller.value, 60); expect(player.volumeChanges, [55, 60]); }); } class _VolumePlayer implements Player { _VolumePlayer(this.volume) : _streams = PlayerStreams( playing: const Stream.empty(), completed: const Stream.empty(), buffering: const Stream.empty(), position: const Stream.empty(), duration: const Stream.empty(), seekable: const Stream.empty(), buffer: const Stream.empty(), volume: const Stream.empty(), rate: const Stream.empty(), tracks: const Stream.empty(), track: const Stream.empty(), log: const Stream.empty(), error: const Stream.empty(), audioDevice: const Stream.empty(), audioDevices: const Stream>.empty(), bufferRanges: const Stream>.empty(), playbackRestart: const Stream.empty(), backendSwitched: const Stream.empty(), ); double volume; final List volumeChanges = []; final PlayerStreams _streams; @override PlayerState get state => PlayerState(volume: volume); @override PlayerStreams get streams => _streams; @override Future setVolume(double volume) async { this.volume = volume; volumeChanges.add(volume); } @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } final class _DelayedVolumePlayer extends _VolumePlayer { _DelayedVolumePlayer(super.volume); final List> _pending = []; final StreamController _volumeController = StreamController.broadcast(); @override PlayerStreams get streams => PlayerStreams( playing: const Stream.empty(), completed: const Stream.empty(), buffering: const Stream.empty(), position: const Stream.empty(), duration: const Stream.empty(), seekable: const Stream.empty(), buffer: const Stream.empty(), volume: _volumeController.stream, rate: const Stream.empty(), tracks: const Stream.empty(), track: const Stream.empty(), log: const Stream.empty(), error: const Stream.empty(), audioDevice: const Stream.empty(), audioDevices: const Stream>.empty(), bufferRanges: const Stream>.empty(), playbackRestart: const Stream.empty(), backendSwitched: const Stream.empty(), ); @override Future setVolume(double requested) async { volumeChanges.add(requested); final completer = Completer(); _pending.add(completer); await completer.future; volume = requested; _volumeController.add(requested); } void completeNext() { _pending.firstWhere((completer) => !completer.isCompleted).complete(); } Future close() => _volumeController.close(); }