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/media/media_source_info.dart'; import 'package:plezy/mpv/mpv.dart'; import 'package:plezy/widgets/overlay_sheet.dart'; import 'package:plezy/widgets/video_controls/sheets/chapter_sheet.dart'; import '../test_helpers/theme.dart'; void main() { setUp(() => LocaleSettings.setLocaleSync(AppLocale.en)); testWidgets('denied chapter tile stays open and cannot seek by pointer or select', (tester) async { final player = _FakePlayer(); await _pumpSheet(tester, player: player, canControl: false); await tester.tap(find.text('Chapter One')); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pump(); expect(player.seeks, isEmpty); expect(find.text('Chapter One'), findsOneWidget); }); testWidgets('authorized chapter seeks, reports completion, and closes', (tester) async { final player = _FakePlayer(); final completions = []; await _pumpSheet(tester, player: player, canControl: true, onCompleted: completions.add); await tester.tap(find.text('Chapter One')); await tester.pumpAndSettle(); expect(player.seeks, [const Duration(seconds: 10)]); expect(completions, [const Duration(seconds: 10)]); expect(find.text('Chapter One'), findsNothing); }); } Future _pumpSheet( WidgetTester tester, { required _FakePlayer player, required bool canControl, ValueChanged? onCompleted, }) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(extensions: const [testMonoTokens]), home: OverlaySheetHost( child: Scaffold( body: Builder( builder: (context) => ElevatedButton( onPressed: () => OverlaySheetController.of(context).show( builder: (_) => ChapterSheet( player: player, chapters: [MediaChapter(id: 1, startTimeOffset: 10000, title: 'Chapter One')], chaptersLoaded: true, canControl: canControl, onSeekCompleted: onCompleted, ), ), child: const Text('Open'), ), ), ), ), ), ); await tester.tap(find.text('Open')); await tester.pumpAndSettle(); } class _FakePlayer implements Player { final List seeks = []; @override PlayerState get state => PlayerState(duration: const Duration(minutes: 30)); @override PlayerStreams get streams => const PlayerStreams( playing: Stream.empty(), completed: Stream.empty(), buffering: Stream.empty(), position: Stream.empty(), duration: Stream.empty(), seekable: Stream.empty(), buffer: Stream.empty(), volume: Stream.empty(), rate: Stream.empty(), tracks: Stream.empty(), track: Stream.empty(), log: Stream.empty(), error: Stream.empty(), audioDevice: Stream.empty(), audioDevices: Stream>.empty(), bufferRanges: Stream>.empty(), playbackRestart: Stream.empty(), backendSwitched: Stream.empty(), ); @override Future seek(Duration position) async => seeks.add(position); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); }