diff --git a/packages/fwfh_just_audio/lib/src/audio_player/audio_player.dart b/packages/fwfh_just_audio/lib/src/audio_player/audio_player.dart index 62c72e96a..3a3cebd86 100644 --- a/packages/fwfh_just_audio/lib/src/audio_player/audio_player.dart +++ b/packages/fwfh_just_audio/lib/src/audio_player/audio_player.dart @@ -91,10 +91,8 @@ class _AudioPlayerState extends State { final theme = Theme.of(context); final fontSize = DefaultTextStyle.of(context).style.fontSize ?? 14.0; - // TODO: remove lint ignore when our minimum Flutter version >= 3.16 - // ignore: deprecated_member_use - final tsf = MediaQuery.textScaleFactorOf(context); - final iconSize = fontSize * tsf; + final tsf = MediaQuery.textScalerOf(context); + final iconSize = tsf.scale(fontSize); return DecoratedBox( decoration: BoxDecoration( @@ -191,10 +189,7 @@ class _PositionText extends StatelessWidget { return Text( text, style: TextStyle(fontSize: size), - - // TODO: remove lint ignore when our minimum Flutter version >= 3.16 - // ignore: deprecated_member_use - textScaleFactor: 1, + textScaler: TextScaler.noScaling, ); }, stream: positionStream, diff --git a/packages/fwfh_just_audio/pubspec.yaml b/packages/fwfh_just_audio/pubspec.yaml index 84cdb5cc2..d2e7226f1 100644 --- a/packages/fwfh_just_audio/pubspec.yaml +++ b/packages/fwfh_just_audio/pubspec.yaml @@ -4,14 +4,14 @@ description: WidgetFactory extension to render AUDIO with the just_audio plugin. homepage: https://github.com/daohoangson/flutter_widget_from_html environment: - flutter: ">=3.10.0" - sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.27.0" + sdk: ">=3.6.0 <4.0.0" dependencies: flutter: sdk: flutter flutter_widget_from_html_core: ">=0.8.0 <0.17.0" - just_audio: ">=0.9.0 <0.10.0" + just_audio: ^0.10.4 dependency_overrides: flutter_widget_from_html_core: @@ -21,8 +21,9 @@ dev_dependencies: flutter_test: sdk: flutter golden_toolkit: ^0.15.0 - just_audio_platform_interface: ">=4.0.0 <5.0.0" + just_audio_platform_interface: ^4.5.0 lint: any + plugin_platform_interface: any tuple: any flutter: diff --git a/packages/fwfh_just_audio/test/audio_player_test.dart b/packages/fwfh_just_audio/test/audio_player_test.dart index e08bd073d..7b0077f92 100644 --- a/packages/fwfh_just_audio/test/audio_player_test.dart +++ b/packages/fwfh_just_audio/test/audio_player_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -10,10 +9,7 @@ import 'package:just_audio_platform_interface/just_audio_platform_interface.dart import 'package:tuple/tuple.dart'; import '../../core/test/_.dart' as core; - -final _commands = []; -late Duration _duration; -late StreamController _playbackEvents; +import 'mock_just_audio_platform.dart'; Future main() async { await loadAppFonts(); @@ -22,12 +18,10 @@ Future main() async { const src = 'http://domain.com/audio.mp3'; group('AudioPlayer', () { - JustAudioPlatform.instance = _JustAudioPlatform(); + mockJustAudioPlatform(); setUp(() { - _commands.clear(); - _duration = const Duration(milliseconds: 10); - _playbackEvents = StreamController.broadcast(); + initializeMockPlatform(); TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( @@ -37,7 +31,7 @@ Future main() async { }); tearDown(() { - _playbackEvents.close(); + disposeMockPlatform(); }); testWidgets('plays then pauses on completion', (tester) async { @@ -49,27 +43,25 @@ Future main() async { ), ); - final playArrow = find.byIcon(Icons.play_arrow); - await tester.tap(playArrow); - await tester.runAsync(() => Future.delayed(Duration.zero)); + await tester.tapWithAsyncDelay(find.byIcon(Icons.play_arrow)); expect( - _commands, + commands, equals(const [ - Tuple2(_CommandType.setVolume, 1.0), - Tuple2(_CommandType.play, null), - Tuple2(_CommandType.load, src), + Tuple2(CommandType.setVolume, 1.0), + Tuple2(CommandType.play, null), + Tuple2(CommandType.load, src), ]), ); - _commands.clear(); + commands.clear(); // simulate a completed event - _playbackEvents.add( + playbackEvents.add( PlaybackEventMessage( processingState: ProcessingStateMessage.completed, updateTime: DateTime.now(), - updatePosition: _duration, - bufferedPosition: _duration, - duration: _duration, + updatePosition: duration, + bufferedPosition: duration, + duration: duration, icyMetadata: null, currentIndex: 0, androidAudioSessionId: null, @@ -77,23 +69,22 @@ Future main() async { ); await tester.runAsync(() => Future.delayed(Duration.zero)); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); expect( - _commands, - equals(const [ - Tuple2(_CommandType.pause, null), - Tuple2(_CommandType.seek, Duration.zero), + commands, + containsAll(const [ + Tuple2(CommandType.pause, null), + Tuple2(CommandType.seek, Duration.zero), ]), ); + expect(commands.length, equals(2)); }); testWidgets('shows remaining (narrow)', (tester) async { tester.setWindowSize(const Size(320, 568)); - _duration = const Duration(minutes: 12, seconds: 34); + duration = const Duration(minutes: 12, seconds: 34); await tester.pumpWidget( const MaterialApp( @@ -104,16 +95,13 @@ Future main() async { ); expect(find.text('-0:00'), findsOneWidget); - await tester.pumpAndSettle(); - expect(find.text('-12:34'), findsOneWidget); + await tester.waitForTextUpdate('-12:34'); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); }); testWidgets('shows position & duration (wide)', (tester) async { - _duration = const Duration(minutes: 12, seconds: 34); + duration = const Duration(minutes: 12, seconds: 34); await tester.pumpWidget( const MaterialApp( @@ -124,16 +112,13 @@ Future main() async { ); expect(find.text('0:00 / 0:00'), findsOneWidget); - await tester.pumpAndSettle(); - expect(find.text('0:00 / 12:34'), findsOneWidget); + await tester.waitForTextUpdate('0:00 / 12:34'); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); }); testWidgets('seeks', (tester) async { - _duration = const Duration(seconds: 100); + duration = const Duration(seconds: 100); await tester.pumpWidget( const MaterialApp( @@ -142,26 +127,23 @@ Future main() async { ), ), ); - await tester.pumpAndSettle(); - expect(find.text('0:00 / 1:40'), findsOneWidget); + + await tester.waitForTextUpdate('0:00 / 1:40'); expect( - _commands, + commands, equals(const [ - Tuple2(_CommandType.setVolume, 1.0), - Tuple2(_CommandType.load, src), + Tuple2(CommandType.setVolume, 1.0), + Tuple2(CommandType.load, src), ]), ); - _commands.clear(); + commands.clear(); - await tester.tap(find.byType(Slider)); - await tester.runAsync(() => Future.delayed(Duration.zero)); + await tester.tapWithAsyncDelay(find.byType(Slider)); await tester.pumpAndSettle(); expect(find.text('0:50 / 1:40'), findsOneWidget); - expect(_commands, equals([Tuple2(_CommandType.seek, _duration * .5)])); + expect(commands, equals([Tuple2(CommandType.seek, duration * .5)])); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); }); group('mute', () { @@ -178,18 +160,15 @@ Future main() async { ); await tester.pumpAndSettle(); - _commands.clear(); + commands.clear(); - await tester.tap(find.byIcon(iconOn)); - await tester.runAsync(() => Future.delayed(Duration.zero)); + await tester.tapWithAsyncDelay(find.byIcon(iconOn)); await tester.pumpAndSettle(); - expect(_commands, equals(const [Tuple2(_CommandType.setVolume, 0.0)])); + expect(commands, equals(const [Tuple2(CommandType.setVolume, 0.0)])); expect(find.byIcon(iconOff), findsOneWidget); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); }); testWidgets('shows muted and unmutes', (tester) async { @@ -201,20 +180,16 @@ Future main() async { ), ); - await tester.runAsync(() => Future.delayed(Duration.zero)); - await tester.pumpAndSettle(); - _commands.clear(); + await tester.waitForCommandUpdate(CommandType.load); + commands.clear(); - await tester.tap(find.byIcon(iconOff)); - await tester.runAsync(() => Future.delayed(Duration.zero)); + await tester.tapWithAsyncDelay(find.byIcon(iconOff)); await tester.pumpAndSettle(); - expect(_commands, equals(const [Tuple2(_CommandType.setVolume, 1.0)])); + expect(commands, equals(const [Tuple2(CommandType.setVolume, 1.0)])); expect(find.byIcon(iconOn), findsOneWidget); - // force a widget tree disposal - await tester.pumpWidget(const SizedBox.shrink()); - await tester.pumpAndSettle(); + await tester.cleanupWidget(); }); }); @@ -255,99 +230,45 @@ Future main() async { }); } -class _JustAudioPlatform extends JustAudioPlatform { - @override - Future init(InitRequest request) async => - _AudioPlayerPlatform(request.id); - - @override - Future disposePlayer( - DisposePlayerRequest request, - ) async => - DisposePlayerResponse(); -} - -class _AudioPlayerPlatform extends AudioPlayerPlatform { - _AudioPlayerPlatform(super.id); - - @override - Stream get playbackEventMessageStream => - _playbackEvents.stream; - - @override - Future load(LoadRequest request) async { - final map = request.audioSourceMessage.toMap(); - _commands.add(Tuple2(_CommandType.load, map['uri'] ?? map)); - - _playbackEvents.add( - PlaybackEventMessage( - processingState: ProcessingStateMessage.ready, - updateTime: DateTime.now(), - updatePosition: Duration.zero, - bufferedPosition: _duration, - duration: _duration, - icyMetadata: null, - currentIndex: 0, - androidAudioSessionId: null, - ), - ); - - return LoadResponse(duration: _duration); - } - - @override - Future play(PlayRequest request) async { - _commands.add(const Tuple2(_CommandType.play, null)); - return PlayResponse(); +extension on WidgetTester { + /// Wait for text to appear with simplified polling + Future waitForTextUpdate(String expectedText) async { + await pumpAndSettle(); + await runAsync(() async { + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 50)); + await pumpAndSettle(); + if (find.text(expectedText).evaluate().isNotEmpty) { + return; + } + } + }); } - @override - Future pause(PauseRequest request) async { - _commands.add(const Tuple2(_CommandType.pause, null)); - return PauseResponse(); + /// Wait for commands with simplified polling + Future waitForCommandUpdate(CommandType commandType) async { + await runAsync(() => Future.delayed(Duration.zero)); + await pumpAndSettle(); + await runAsync(() async { + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 50)); + await pumpAndSettle(); + if (commands.any((cmd) => cmd.item1 == commandType)) { + break; + } + } + }); } - @override - Future setVolume(SetVolumeRequest request) async { - _commands.add(Tuple2(_CommandType.setVolume, request.volume)); - return SetVolumeResponse(); + /// Tap with async delay - for interactions that trigger async operations + Future tapWithAsyncDelay(Finder finder) async { + await tap(finder); + await runAsync(() => Future.delayed(Duration.zero)); } - @override - Future setSpeed(SetSpeedRequest request) async => - SetSpeedResponse(); - - @override - Future setLoopMode(SetLoopModeRequest request) async => - SetLoopModeResponse(); - - @override - Future setShuffleMode( - SetShuffleModeRequest request, - ) async => - SetShuffleModeResponse(); - - @override - Future seek(SeekRequest request) async { - _commands.add(Tuple2(_CommandType.seek, request.position)); - return SeekResponse(); + /// Standard cleanup + Future cleanupWidget() async { + await pumpWidget(const SizedBox.shrink()); + await pumpAndSettle(); } - - @override - Future setAndroidAudioAttributes( - SetAndroidAudioAttributesRequest request, - ) async => - SetAndroidAudioAttributesResponse(); - - @override - Future dispose(DisposeRequest request) async => - DisposeResponse(); -} - -enum _CommandType { - load, - pause, - play, - seek, - setVolume, } diff --git a/packages/fwfh_just_audio/test/mock_just_audio_platform.dart b/packages/fwfh_just_audio/test/mock_just_audio_platform.dart new file mode 100644 index 000000000..ea5f4e1f7 --- /dev/null +++ b/packages/fwfh_just_audio/test/mock_just_audio_platform.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:tuple/tuple.dart'; + +final commands = []; +late Duration duration; +late StreamController playbackEvents; + +void mockJustAudioPlatform() { + _FakeJustAudioPlatform(); +} + +void initializeMockPlatform() { + commands.clear(); + duration = const Duration(milliseconds: 10); + playbackEvents = StreamController.broadcast(); +} + +void disposeMockPlatform() { + playbackEvents.close(); +} + +class _FakeJustAudioPlatform extends Fake + with MockPlatformInterfaceMixin + implements JustAudioPlatform { + _FakeJustAudioPlatform() { + JustAudioPlatform.instance = this; + } + + @override + Future init(InitRequest request) async => + _FakeAudioPlayerPlatform(request.id); + + @override + Future disposePlayer( + DisposePlayerRequest request, + ) async => + DisposePlayerResponse(); +} + +class _FakeAudioPlayerPlatform extends Fake implements AudioPlayerPlatform { + final String _id; + + _FakeAudioPlayerPlatform(this._id); + + @override + String get id => _id; + + @override + Stream get playbackEventMessageStream => + playbackEvents.stream; + + @override + Stream get playerDataMessageStream => const Stream.empty(); + + @override + Future load(LoadRequest request) async { + final map = request.audioSourceMessage.toMap(); + + // Extract the URI from the audio source message structure + String? uri; + if (map['type'] == 'concatenating') { + // For concatenating sources, extract URI from the first child + final children = map['children'] as List?; + if (children != null && children.isNotEmpty) { + final firstChild = children[0] as Map?; + uri = firstChild?['uri'] as String?; + } + } else { + // For single sources, extract URI directly + uri = map['uri'] as String?; + } + + commands.add(Tuple2(CommandType.load, uri ?? map)); + + // Send multiple events to properly simulate the loading process + playbackEvents.add( + PlaybackEventMessage( + processingState: ProcessingStateMessage.loading, + updateTime: DateTime.now(), + updatePosition: Duration.zero, + bufferedPosition: Duration.zero, + duration: null, + icyMetadata: null, + currentIndex: 0, + androidAudioSessionId: null, + ), + ); + + // Small delay to simulate loading + await Future.delayed(Duration.zero); + + playbackEvents.add( + PlaybackEventMessage( + processingState: ProcessingStateMessage.ready, + updateTime: DateTime.now(), + updatePosition: Duration.zero, + bufferedPosition: duration, + duration: duration, + icyMetadata: null, + currentIndex: 0, + androidAudioSessionId: null, + ), + ); + + return LoadResponse(duration: duration); + } + + @override + Future play(PlayRequest request) async { + commands.add(const Tuple2(CommandType.play, null)); + return PlayResponse(); + } + + @override + Future pause(PauseRequest request) async { + commands.add(const Tuple2(CommandType.pause, null)); + return PauseResponse(); + } + + @override + Future setVolume(SetVolumeRequest request) async { + commands.add(Tuple2(CommandType.setVolume, request.volume)); + return SetVolumeResponse(); + } + + @override + Future seek(SeekRequest request) async { + commands.add(Tuple2(CommandType.seek, request.position)); + return SeekResponse(); + } + + @override + Future setSpeed(SetSpeedRequest request) async => + SetSpeedResponse(); + + @override + Future setLoopMode(SetLoopModeRequest request) async => + SetLoopModeResponse(); + + @override + Future setShuffleMode( + SetShuffleModeRequest request, + ) async => + SetShuffleModeResponse(); + + @override + Future dispose(DisposeRequest request) async => + DisposeResponse(); +} + +enum CommandType { + load, + pause, + play, + seek, + setVolume, +}