Files
ReelShort/test/player/player_open_detail_test.dart
T
Vicky 79463e65c2 feat(player): 剧名与选集合并为同一个弹窗
点剧名默认停在详情页签,选集入口直接停在选集页签,不再跳详情页;
详情页签补上简介、标签、收藏与相似推荐,原选集弹窗合并进来后删除。
2026-10-29 15:44:10 +08:00

238 lines
7.6 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), findsNothing);
expect(find.byKey(const Key('drama-info-sheet')), findsOneWidget);
expect(find.byKey(const Key('drama-info-tab-detail')), findsOneWidget);
expect(find.byKey(const Key('drama-info-detail')), 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.tap(find.byKey(const Key('drama-info-close')));
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();
}
}