import 'dart:ui' show Tristate; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/i18n/strings.g.dart'; import 'package:plezy/media/media_source_info.dart'; import 'package:plezy/mpv/mpv.dart'; import 'package:plezy/theme/mono_tokens.dart'; import 'package:plezy/widgets/video_controls/models/track_controls_state.dart'; import 'package:plezy/widgets/video_controls/sheets/track_sheet.dart'; const _testTokens = MonoTokens( radiusSm: 8, radiusMd: 12, radiusLg: 20, radiusXs: 5, groupGap: 2, space: 8, fast: Duration(milliseconds: 1), normal: Duration(milliseconds: 1), slow: Duration(milliseconds: 1), expressive: Duration(milliseconds: 1), bg: Colors.black, surface: Colors.black, outline: Colors.white24, text: Colors.white, textMuted: Colors.white70, splashFactory: NoSplash.splashFactory, ); void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { LocaleSettings.setLocaleSync(AppLocale.en); }); group('TrackSheet subtitle controls', () { testWidgets('shows subtitle search when Plex search is available without subtitle tracks', (tester) async { final player = _FakeTrackSheetPlayer( tracks: const Tracks( audio: [ AudioTrack(id: 'a1'), AudioTrack(id: 'a2'), ], ), track: const TrackSelection( audio: AudioTrack(id: 'a1'), subtitle: SubtitleTrack.off, ), ); await _pumpTrackSheet( tester, player: player, trackControlsState: const TrackControlsState( ratingKey: '123', serverId: 'plex-server', subtitleSearchSupported: true, ), ); expect(find.text('Search Subtitles'), findsOneWidget); }); testWidgets('hides subtitle search when external subtitle search is unsupported', (tester) async { final player = _FakeTrackSheetPlayer( tracks: const Tracks( audio: [ AudioTrack(id: 'a1'), AudioTrack(id: 'a2'), ], ), track: const TrackSelection( audio: AudioTrack(id: 'a1'), subtitle: SubtitleTrack.off, ), ); await _pumpTrackSheet( tester, player: player, trackControlsState: const TrackControlsState( ratingKey: '123', serverId: 'jellyfin-server', subtitleSearchSupported: false, ), ); expect(find.text('Search Subtitles'), findsNothing); }); }); group('TrackSheet two-line labels', () { testWidgets('renders language as the primary line and tech detail below', (tester) async { final player = _FakeTrackSheetPlayer( tracks: const Tracks( audio: [ AudioTrack(id: 'a1', language: 'eng', codec: 'aac', channels: 2), AudioTrack( id: 'a2', title: 'Dolby Digital Plus 5.1 with Atmos', language: 'ta', codec: 'eac3', channels: 6, ), ], ), track: const TrackSelection( audio: AudioTrack(id: 'a1', language: 'eng', codec: 'aac', channels: 2), subtitle: SubtitleTrack.off, ), ); await _pumpTrackSheet( tester, player: player, trackControlsState: const TrackControlsState(subtitleSearchSupported: false), ); expect(find.text('English'), findsOneWidget); expect(find.text('AAC · Stereo'), findsOneWidget); expect(find.text('Tamil'), findsOneWidget); expect(find.text('Dolby Digital Plus 5.1 with Atmos · E-AC3 · 5.1'), findsOneWidget); final englishTile = find.ancestor(of: find.text('English'), matching: find.byType(ListTile)); final tamilTile = find.ancestor(of: find.text('Tamil'), matching: find.byType(ListTile)); expect(tester.getSemantics(englishTile).getSemanticsData().flagsCollection.isSelected, Tristate.isTrue); expect(tester.getSemantics(tamilTile).getSemanticsData().flagsCollection.isSelected, Tristate.isFalse); }); }); group('TrackControlsState.hasSubtitleControls', () { test('counts source subtitles only when source switching is available', () { final sourceSubtitle = MediaSubtitleTrack(id: 1, selected: false, forced: false); expect( TrackControlsState( isTranscoding: true, sourceSubtitleTracks: [sourceSubtitle], ).hasSubtitleControls(const Tracks()), isFalse, ); expect( TrackControlsState( isTranscoding: true, sourceSubtitleTracks: [sourceSubtitle], onSwitchSubtitleStreamId: (_) {}, ).hasSubtitleControls(const Tracks()), isTrue, ); }); test('ignores player subtitle placeholders', () { const state = TrackControlsState(subtitleSearchSupported: false); expect(state.hasSubtitleControls(const Tracks(subtitle: [SubtitleTrack.auto, SubtitleTrack.off])), isFalse); expect(state.hasSubtitleControls(const Tracks(subtitle: [SubtitleTrack(id: 's1')])), isTrue); }); }); } Future _pumpTrackSheet( WidgetTester tester, { required Player player, required TrackControlsState trackControlsState, }) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(extensions: const [_testTokens]), home: Scaffold( body: SizedBox( width: 700, height: 400, child: TrackSheet(player: player, trackControlsState: trackControlsState), ), ), ), ); await tester.pump(); } class _FakeTrackSheetPlayer implements Player { _FakeTrackSheetPlayer({required Tracks tracks, required TrackSelection track}) : _state = PlayerState(tracks: tracks, track: track), _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(), ); final PlayerState _state; final PlayerStreams _streams; @override PlayerState get state => _state; @override PlayerStreams get streams => _streams; @override bool get supportsSecondarySubtitles => false; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); }