235 lines
7.4 KiB
Dart
235 lines
7.4 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:fruitshorts/core/data/models/drama.dart';
|
|
import 'package:fruitshorts/core/data/models/player_port.dart';
|
|
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
|
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
|
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
|
import 'package:fruitshorts/core/data/services/android_picture_in_picture.dart';
|
|
import 'package:fruitshorts/core/data/services/catalog_controller.dart';
|
|
import 'package:fruitshorts/core/data/services/favorite_controller.dart';
|
|
import 'package:fruitshorts/core/data/services/playback_position_cache.dart';
|
|
import 'package:fruitshorts/core/data/services/playback_session.dart';
|
|
import 'package:fruitshorts/core/data/services/video_player_port.dart';
|
|
import 'package:fruitshorts/core/platform/haptics.dart';
|
|
import 'package:fruitshorts/l10n/app_localizations.dart';
|
|
import 'package:fruitshorts/pages/drama_detail/drama_detail_view.dart';
|
|
import 'package:fruitshorts/pages/player/player_logic.dart';
|
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
import '../support/fake_catalog.dart';
|
|
import '../support/fake_haptics.dart';
|
|
import '../support/fake_watch_history.dart';
|
|
|
|
void main() {
|
|
testWidgets(
|
|
'should_keep_active_player_and_progress_when_detail_route_is_open',
|
|
(tester) async {
|
|
final drama = _drama();
|
|
final history = FakeWatchHistory();
|
|
final playback = FakePlaybackRepository();
|
|
final catalog = CatalogController(FakeCatalogRepository(dramas: [drama]));
|
|
final positionCache = PlaybackPositionCache();
|
|
final favorites = FavoriteController(
|
|
InMemoryFavoriteRepository(saved: {drama.id}, cards: [drama]),
|
|
);
|
|
final logic = PlayerLogic(
|
|
history: history,
|
|
playback: playback,
|
|
positionCache: positionCache,
|
|
pictureInPicture: AndroidPictureInPicture(
|
|
channel: const MethodChannel('test/player_open_detail/pip'),
|
|
),
|
|
);
|
|
final factory = _VideoRecordingFactory();
|
|
logic.attach(
|
|
PlayerPage(drama: drama, initialIndex: 0),
|
|
isMounted: () => true,
|
|
);
|
|
logic.start(factory: factory.create, coordinator: PlaybackCoordinator());
|
|
await _pumpSession(tester);
|
|
|
|
final active = logic.session.active;
|
|
final index = logic.session.currentIndex;
|
|
final player = factory.created.first;
|
|
final createdBeforeDetail = factory.created.length;
|
|
logic
|
|
..trackedPlayer = player
|
|
..trackedEpisode = drama.episodes.first
|
|
..trackedDrama = drama;
|
|
|
|
late BuildContext pageContext;
|
|
await tester.pumpWidget(
|
|
MultiProvider(
|
|
providers: [
|
|
Provider<WatchHistoryRepository>.value(value: history),
|
|
Provider<PlaybackRepository>.value(value: playback),
|
|
Provider<PlaybackPositionCache>.value(value: positionCache),
|
|
ChangeNotifierProvider<CatalogController>.value(value: catalog),
|
|
ChangeNotifierProvider<FavoriteController>.value(value: favorites),
|
|
Provider<Haptics>.value(value: FakeHaptics()),
|
|
],
|
|
child: MaterialApp(
|
|
locale: const Locale('en'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Builder(
|
|
builder: (context) {
|
|
pageContext = context;
|
|
return const SizedBox.shrink();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
logic.beginScrub(player);
|
|
await logic.seekTo(
|
|
pageContext,
|
|
player,
|
|
const Duration(minutes: 1, seconds: 22),
|
|
);
|
|
|
|
unawaited(logic.openDetail(pageContext));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await _pumpSession(tester);
|
|
await tester.runAsync(() => logic.session.playbackSettled);
|
|
|
|
expect(find.byType(DramaDetailPage), findsOneWidget);
|
|
expect(logic.session.active, same(active));
|
|
expect(logic.session.currentIndex, index);
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
expect(factory.created, hasLength(createdBeforeDetail));
|
|
expect(
|
|
player.controller.value.position,
|
|
const Duration(minutes: 1, seconds: 22),
|
|
);
|
|
expect(history.writes, ['signal-1|82000|false', 'signal-1|82000|false']);
|
|
expect(
|
|
positionCache.read('signal-1', dramaId: 'signal')?.position,
|
|
const Duration(minutes: 1, seconds: 22),
|
|
);
|
|
|
|
await tester.pageBack();
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
await _pumpSession(tester);
|
|
await logic.pendingWrite;
|
|
|
|
expect(logic.session.active, same(active));
|
|
expect(logic.session.currentIndex, index);
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
expect(factory.created, hasLength(createdBeforeDetail));
|
|
expect(
|
|
player.controller.value.position,
|
|
const Duration(minutes: 1, seconds: 22),
|
|
);
|
|
expect(history.writes, [
|
|
'signal-1|82000|false',
|
|
'signal-1|82000|false',
|
|
'signal-1|82000|false',
|
|
]);
|
|
|
|
logic
|
|
..trackedPlayer = null
|
|
..trackedEpisode = null
|
|
..trackedDrama = null
|
|
..dispose();
|
|
await _pumpSession(tester);
|
|
},
|
|
);
|
|
}
|
|
|
|
Drama _drama() => Drama(
|
|
id: 'signal',
|
|
title: 'Signal',
|
|
synopsis: 'synopsis',
|
|
posterUrl: '/files/signal.png',
|
|
episodes: const [
|
|
Episode(
|
|
id: 'signal-1',
|
|
number: 1,
|
|
title: 'Episode 1',
|
|
videoUrl: 'https://example.test/signal-1.mp4',
|
|
isFree: true,
|
|
),
|
|
],
|
|
);
|
|
|
|
Future<void> _pumpSession(WidgetTester tester) async {
|
|
for (var attempt = 0; attempt < 5; attempt++) {
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
}
|
|
}
|
|
|
|
class _VideoRecordingFactory {
|
|
final List<_VideoRecordingPlayer> created = [];
|
|
|
|
PlayerPort create(Episode episode) {
|
|
final player = _VideoRecordingPlayer(episode);
|
|
created.add(player);
|
|
return player;
|
|
}
|
|
}
|
|
|
|
class _VideoRecordingPlayer extends VideoPlayerPort {
|
|
_VideoRecordingPlayer(super.episode);
|
|
|
|
@override
|
|
Future<void> initialize() async {
|
|
controller.value = const VideoPlayerValue(
|
|
duration: Duration(minutes: 5),
|
|
size: Size(1280, 720),
|
|
position: Duration(seconds: 1),
|
|
isInitialized: true,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> play() async {
|
|
_setPlaying(true);
|
|
}
|
|
|
|
@override
|
|
Future<void> pause() async {
|
|
_setPlaying(false);
|
|
}
|
|
|
|
@override
|
|
Future<void> seekTo(Duration position) async {
|
|
final current = controller.value;
|
|
controller.value = VideoPlayerValue(
|
|
duration: current.duration,
|
|
size: current.size,
|
|
position: position,
|
|
isInitialized: current.isInitialized,
|
|
isPlaying: current.isPlaying,
|
|
);
|
|
}
|
|
|
|
void _setPlaying(bool playing) {
|
|
final current = controller.value;
|
|
controller.value = VideoPlayerValue(
|
|
duration: current.duration,
|
|
size: current.size,
|
|
position: current.position,
|
|
isInitialized: current.isInitialized,
|
|
isPlaying: playing,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
await controller.dispose();
|
|
}
|
|
}
|