2049 lines
70 KiB
Dart
2049 lines
70 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:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
|
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
|
|
|
import 'package:fruitshorts/app.dart';
|
|
import 'package:fruitshorts/core/data/models/drama.dart';
|
|
import 'package:fruitshorts/core/data/models/interaction.dart';
|
|
import 'package:fruitshorts/core/data/models/play_ticket.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/playback_session.dart';
|
|
import 'package:fruitshorts/core/data/services/video_player_port.dart';
|
|
import 'package:fruitshorts/core/theme/app_colors.dart';
|
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
|
import 'package:fruitshorts/pages/player/player_logic.dart';
|
|
import 'package:fruitshorts/pages/player/player_view.dart';
|
|
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
|
|
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
|
|
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
import '../support/fake_catalog.dart';
|
|
import '../support/fake_category.dart';
|
|
import '../support/fake_haptics.dart';
|
|
import '../support/fake_interaction_repositories.dart';
|
|
import '../support/fake_notice_repository.dart';
|
|
import '../support/widget_player.dart';
|
|
|
|
void main() {
|
|
setUp(() {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.empty();
|
|
});
|
|
|
|
testWidgets('should_render_immersive_overlay_in_recommend_feed', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget);
|
|
expect(
|
|
tester
|
|
.widget<PlayerPage>(find.byKey(const Key('recommend-feed-player')))
|
|
.homeMode,
|
|
isTrue,
|
|
);
|
|
final pageViews = tester.widgetList<PageView>(find.byType(PageView));
|
|
expect(
|
|
pageViews.where((pageView) => pageView.scrollDirection == Axis.vertical),
|
|
hasLength(1),
|
|
);
|
|
expect(find.byKey(const Key('player-render-stack')), findsOneWidget);
|
|
final renderStackPageAncestors = tester.widgetList<PageView>(
|
|
find.ancestor(
|
|
of: find.byKey(const Key('player-render-stack')),
|
|
matching: find.byType(PageView),
|
|
),
|
|
);
|
|
expect(
|
|
renderStackPageAncestors.where(
|
|
(pageView) => pageView.scrollDirection == Axis.vertical,
|
|
),
|
|
isEmpty,
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('should_use_black_system_navigation_bar_on_feed_and_player', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await _openSeriesPlayer(tester);
|
|
_expectPlayerNavigationBar(tester);
|
|
});
|
|
|
|
testWidgets('should_keep_feed_session_unchanged_when_switching_episode', (
|
|
tester,
|
|
) async {
|
|
final factory = _RouteVideoFactory();
|
|
final playback = FakePlaybackRepository();
|
|
await _pumpApp(tester, playerFactory: factory.create, playback: playback);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final feedPage = find
|
|
.descendant(
|
|
of: find.byKey(const Key('recommend-feed-player')),
|
|
matching: find.byType(PageView),
|
|
)
|
|
.first;
|
|
await tester.drag(feedPage, const Offset(0, -620));
|
|
await tester.pumpAndSettle();
|
|
await tester.drag(feedPage, const Offset(0, -620));
|
|
await tester.pumpAndSettle();
|
|
|
|
final recommendPlayer = find.byKey(const Key('recommend-feed-player'));
|
|
final sourceState = tester.state(recommendPlayer);
|
|
final sourceLogic = _pageLogic(tester, recommendPlayer);
|
|
await sourceLogic.session.preloadSettled;
|
|
await tester.pumpAndSettle();
|
|
final sourceIndex = sourceLogic.session.currentIndex;
|
|
final sourceDramaId = sourceLogic.currentDramaId;
|
|
final sourceEpisodeId = sourceLogic.session.currentEpisode.id;
|
|
final feedPlayer = sourceLogic.session.active! as _RouteVideoPlayer;
|
|
final feedPlayCalls = feedPlayer.playCalls;
|
|
final feedPositionBefore = feedPlayer.controller.value.position;
|
|
final sourceLiveBefore = sourceLogic.session.livePlayerCount;
|
|
final sourceCurrentSurface = find.descendant(
|
|
of: recommendPlayer,
|
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
|
);
|
|
final sourceSurfaceState = tester.state(sourceCurrentSurface);
|
|
final sourceSurfaceElement = tester.element(sourceCurrentSurface);
|
|
expect(sourceDramaId, 'vengeance-demo');
|
|
expect(sourceIndex, 2);
|
|
expect(sourceLiveBefore, greaterThan(1));
|
|
|
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final fullPlayer = _fullPlayerFinder();
|
|
final fullLogic = _pageLogic(tester, fullPlayer);
|
|
expect(fullLogic.session, isNot(same(sourceLogic.session)));
|
|
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
|
|
expect(fullPlayerPort.controller, isNot(same(feedPlayer.controller)));
|
|
expect(feedPlayer.disposed, isFalse);
|
|
expect(feedPlayer.playing, isFalse);
|
|
|
|
final fullCurrentSurface = find.descendant(
|
|
of: fullPlayer,
|
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
|
);
|
|
final fullCurrentSurfaceState = tester.state(fullCurrentSurface);
|
|
final loadingOverlay = find.byKey(
|
|
const Key('player-entry-loading-overlay'),
|
|
);
|
|
expect(loadingOverlay, findsNothing);
|
|
final fullPlayerPage = find
|
|
.descendant(
|
|
of: _fullPlayerFinder(),
|
|
matching: find.byWidgetPredicate(
|
|
(widget) =>
|
|
widget is PageView && widget.scrollDirection == Axis.vertical,
|
|
),
|
|
)
|
|
.first;
|
|
for (var index = 0; index < 4; index++) {
|
|
await tester.drag(fullPlayerPage, const Offset(0, -560));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(loadingOverlay, findsNothing);
|
|
}
|
|
|
|
expect(fullLogic.session.currentEpisode.number, 5);
|
|
expect(tester.state(fullCurrentSurface), same(fullCurrentSurfaceState));
|
|
expect(
|
|
fullLogic.queue.entryAt(fullLogic.session.currentIndex).drama.id,
|
|
sourceDramaId,
|
|
);
|
|
expect(fullLogic.session.userWantsPlay, isTrue);
|
|
expect(sourceLogic.session.currentIndex, sourceIndex);
|
|
expect(sourceLogic.currentDramaId, sourceDramaId);
|
|
expect(sourceLogic.session.currentEpisode.id, sourceEpisodeId);
|
|
expect(sourceLogic.session.active, same(feedPlayer));
|
|
expect(feedPlayer.disposed, isFalse);
|
|
|
|
final requestsBeforeReturn = playback.requests.length;
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-back')).hitTestable());
|
|
await tester.pumpAndSettle();
|
|
await sourceLogic.session.preloadSettled;
|
|
await tester.pumpAndSettle();
|
|
|
|
final currentSurface = find.descendant(
|
|
of: recommendPlayer,
|
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
|
);
|
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
|
expect(tester.state(recommendPlayer), same(sourceState));
|
|
expect(sourceLogic.session.currentIndex, sourceIndex);
|
|
expect(sourceLogic.currentDramaId, sourceDramaId);
|
|
expect(sourceLogic.session.currentEpisode.id, sourceEpisodeId);
|
|
expect(sourceLogic.session.active, same(feedPlayer));
|
|
expect(feedPlayer.disposed, isFalse);
|
|
expect(sourceLogic.active, isTrue);
|
|
expect(sourceLogic.session.userWantsPlay, isTrue);
|
|
expect(sourceLogic.session.canResumeAfterBackground, isTrue);
|
|
expect(sourceLogic.session.canAutoAdvance, isTrue);
|
|
expect(sourceLogic.session.isLocked, isFalse);
|
|
expect(sourceLogic.session.loading, isFalse);
|
|
expect(sourceLogic.session.activeSlotActivated, isTrue);
|
|
expect(sourceLogic.session.activeSlotPlaying, isTrue);
|
|
expect(feedPlayer.seekCalls, 0);
|
|
expect(feedPlayer.playCalls, greaterThan(feedPlayCalls));
|
|
expect(playback.requests.length, requestsBeforeReturn);
|
|
expect(feedPlayer.controller.value.position, feedPositionBefore);
|
|
expect(feedPlayer.playing, isTrue);
|
|
expect(sourceLogic.session.livePlayerCount, sourceLiveBefore);
|
|
expect(sourceLogic.session.livePlayerCount, greaterThan(1));
|
|
expect(tester.state(currentSurface), same(sourceSurfaceState));
|
|
expect(tester.element(currentSurface), same(sourceSurfaceElement));
|
|
expect(
|
|
find.descendant(
|
|
of: currentSurface,
|
|
matching: find.byKey(const Key('player-single-placeholder')),
|
|
),
|
|
findsNothing,
|
|
);
|
|
expect(
|
|
tester.widget<PlayerVideoSurface>(currentSurface).controller,
|
|
same(feedPlayer.controller),
|
|
);
|
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
|
expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_show_one_paused_overlay_when_entering_full_player', (
|
|
tester,
|
|
) async {
|
|
final factory = _RouteVideoFactory();
|
|
await _pumpApp(tester, playerFactory: factory.create);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('home-feed-gesture')));
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(_fullPlayerFinder(), findsOneWidget);
|
|
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
|
expect(
|
|
find.byKey(const Key('home-feed-center-play')).hitTestable(),
|
|
findsNothing,
|
|
);
|
|
expect(find.byKey(const Key('home-tabs-bar')).hitTestable(), findsNothing);
|
|
expect(
|
|
find.byKey(const Key('recommend-top-nav')).hitTestable(),
|
|
findsNothing,
|
|
);
|
|
|
|
await tester.tap(find.byKey(const Key('player-back')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
|
|
expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_show_non_blocking_loading_when_opening_series', (
|
|
tester,
|
|
) async {
|
|
final initialization = Completer<void>();
|
|
final routeFactory = _RouteVideoFactory();
|
|
await _pumpApp(
|
|
tester,
|
|
playerFactory: (episode) => episode.id == 'the-signal-demo-2'
|
|
? _GatedVideoPlayer(episode, initialization.future)
|
|
: routeFactory.create(episode),
|
|
);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('rail-episodes')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-episode-2')));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
final loadingOverlay = find.byKey(
|
|
const Key('player-entry-loading-overlay'),
|
|
);
|
|
final fullPlayer = find.byType(PlayerPage).last;
|
|
final fullLogic = _pageLogic(tester, fullPlayer);
|
|
final fullCurrentSurface = find.descendant(
|
|
of: fullPlayer,
|
|
matching: find.byKey(const ValueKey('player-render-slot-current')),
|
|
);
|
|
final fullCurrentSurfaceState = tester.state(fullCurrentSurface);
|
|
final fullPlayerPage = find
|
|
.descendant(
|
|
of: fullPlayer,
|
|
matching: find.byWidgetPredicate(
|
|
(widget) =>
|
|
widget is PageView && widget.scrollDirection == Axis.vertical,
|
|
),
|
|
)
|
|
.first;
|
|
expect(loadingOverlay, findsOneWidget);
|
|
expect(tester.widget<IgnorePointer>(loadingOverlay).ignoring, isTrue);
|
|
expect(tester.getSize(loadingOverlay), const Size(40, 40));
|
|
expect(
|
|
tester
|
|
.widget<AnimatedOpacity>(
|
|
find.descendant(
|
|
of: loadingOverlay,
|
|
matching: find.byType(AnimatedOpacity),
|
|
),
|
|
)
|
|
.duration,
|
|
const Duration(milliseconds: 140),
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-loading-surface'))),
|
|
const Size(40, 40),
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-entry-loading'))),
|
|
const Size(20, 20),
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<CircularProgressIndicator>(
|
|
find.byKey(const Key('player-entry-loading')),
|
|
)
|
|
.color,
|
|
Colors.white,
|
|
);
|
|
final loadingSurface = tester.widget<Container>(
|
|
find.byKey(const Key('player-loading-surface')),
|
|
);
|
|
final decoration = loadingSurface.decoration! as BoxDecoration;
|
|
expect(decoration.color, AppColors.refreshIndicatorSurface);
|
|
expect(decoration.borderRadius, BorderRadius.circular(20));
|
|
expect(
|
|
find.descendant(of: loadingOverlay, matching: find.byType(ColoredBox)),
|
|
findsNothing,
|
|
);
|
|
|
|
expect(find.byKey(const Key('player-back')).hitTestable(), findsOneWidget);
|
|
expect(
|
|
find.byKey(const Key('player-episode-heading')).hitTestable(),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.byKey(const Key('player-drama-title')).hitTestable(),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find
|
|
.descendant(
|
|
of: find.byKey(const Key('player-rail-like')),
|
|
matching: find.byType(IconButton),
|
|
)
|
|
.hitTestable(),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.text('00:00 / 01:00'), findsOneWidget);
|
|
|
|
// IgnorePointer 让加载中的播放暂停与上下滑穿透,滑动会关闭入口 loading。
|
|
expect(fullLogic.session.userWantsPlay, isTrue);
|
|
await tester.tap(find.byKey(const Key('player-video-tap')).hitTestable());
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(fullLogic.session.userWantsPlay, isFalse);
|
|
final indexBeforeSwipe = fullLogic.session.currentIndex;
|
|
await tester.drag(fullPlayerPage, const Offset(0, -560));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
expect(fullLogic.session.currentIndex, isNot(indexBeforeSwipe));
|
|
expect(tester.state(fullCurrentSurface), same(fullCurrentSurfaceState));
|
|
|
|
// 切集完成后返回按钮仍可正常点击。
|
|
await tester.pumpAndSettle();
|
|
expect(loadingOverlay, findsNothing);
|
|
await tester.tap(find.byKey(const Key('player-back')).hitTestable());
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
|
initialization.complete();
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets('should_keep_drama_and_episode_indices_separate', (tester) async {
|
|
final factory = _RouteVideoFactory();
|
|
await _pumpApp(tester, playerFactory: factory.create);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final feedPage = find
|
|
.descendant(
|
|
of: find.byKey(const Key('recommend-feed-player')),
|
|
matching: find.byType(PageView),
|
|
)
|
|
.first;
|
|
await tester.drag(feedPage, const Offset(0, -620));
|
|
await tester.pumpAndSettle();
|
|
final recommendPlayer = find.byKey(const Key('recommend-feed-player'));
|
|
final sourceLogic = _pageLogic(tester, recommendPlayer);
|
|
final feedIndex = sourceLogic.session.currentIndex;
|
|
final feedEpisodeId = sourceLogic.session.currentEpisode.id;
|
|
final feedDramaId = sourceLogic.currentDramaId;
|
|
expect(feedIndex, 1);
|
|
|
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
|
|
await tester.drag(
|
|
find.byKey(const Key('player-video-tap')),
|
|
const Offset(0, -560),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(fullLogic.session.currentIndex, 1);
|
|
expect(fullLogic.session.currentEpisode.number, 2);
|
|
expect(fullLogic.queue.entryAt(1).drama.id, feedDramaId);
|
|
expect(sourceLogic.session.currentIndex, feedIndex);
|
|
expect(sourceLogic.session.currentEpisode.id, feedEpisodeId);
|
|
expect(sourceLogic.currentDramaId, feedDramaId);
|
|
|
|
await tester.tap(find.byKey(const Key('player-back')));
|
|
await tester.pumpAndSettle();
|
|
expect(sourceLogic.session.currentIndex, feedIndex);
|
|
expect(sourceLogic.session.currentEpisode.id, feedEpisodeId);
|
|
});
|
|
|
|
testWidgets('should_render_player_loading_indicators_at_compact_size', (
|
|
tester,
|
|
) async {
|
|
final playback = _PendingPlaybackRepository();
|
|
final initialization = Completer<void>();
|
|
await _pumpApp(
|
|
tester,
|
|
playback: playback,
|
|
playerFactory: (_) => _GatedPlayer(initialization.future),
|
|
settle: false,
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
final ticketLoading = find.byKey(const Key('player-ticket-loading'));
|
|
expect(ticketLoading, findsOneWidget);
|
|
expect(tester.getSize(ticketLoading), const Size(24, 24));
|
|
expect(
|
|
tester.widget<CircularProgressIndicator>(ticketLoading).strokeWidth,
|
|
2,
|
|
);
|
|
|
|
playback.ticket.complete(
|
|
const PlayTicket(
|
|
episodeId: 'city-lights-demo-1',
|
|
accessible: true,
|
|
accessType: PlayTicket.free,
|
|
playUrl: '/files/episode/fake-city.mp4?e=1&s=abc',
|
|
qualities: ['AUTO'],
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 100));
|
|
|
|
expect(find.byKey(const Key('player-session-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
|
|
initialization.complete();
|
|
await tester.pumpWidget(const SizedBox.shrink());
|
|
await tester.pump();
|
|
});
|
|
|
|
testWidgets('should_pause_and_resume_player_when_page_route_covers_it', (
|
|
tester,
|
|
) async {
|
|
final players = <_RecordingPlayer>[];
|
|
await _pumpApp(
|
|
tester,
|
|
playerFactory: (_) {
|
|
final player = _RecordingPlayer();
|
|
players.add(player);
|
|
return player;
|
|
},
|
|
);
|
|
await _openSeriesPlayer(tester);
|
|
final player = players.firstWhere((item) => item.playing);
|
|
expect(player.playing, isTrue);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
|
|
|
|
final navigator = tester.state<NavigatorState>(
|
|
find.byType(Navigator).first,
|
|
);
|
|
unawaited(
|
|
navigator.push<void>(
|
|
MaterialPageRoute(
|
|
builder: (_) => const Scaffold(key: Key('test-covering-route')),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('test-covering-route')), findsOneWidget);
|
|
expect(player.playing, isFalse);
|
|
|
|
navigator.pop();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(player.playing, isTrue);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_pause_feed_when_auth_page_is_pushed_and_resume_on_pop', (
|
|
tester,
|
|
) async {
|
|
final players = <_RecordingPlayer>[];
|
|
await _pumpApp(
|
|
tester,
|
|
playerFactory: (_) {
|
|
final player = _RecordingPlayer();
|
|
players.add(player);
|
|
return player;
|
|
},
|
|
);
|
|
await tester.tap(find.byKey(const Key('tab-recommend')));
|
|
await tester.pumpAndSettle();
|
|
final player = players.firstWhere((item) => item.playing);
|
|
expect(player.playing, isTrue);
|
|
|
|
await tester.tap(find.byKey(const Key('rail-save')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('Sign In'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(AuthPage), findsOneWidget);
|
|
expect(player.playing, isFalse);
|
|
|
|
await tester.pageBack();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(player.playing, isTrue);
|
|
});
|
|
|
|
testWidgets('should_keep_video_surface_background_black', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(
|
|
tester
|
|
.widget<ColoredBox>(
|
|
find.byKey(const Key('player-fallback-background')),
|
|
)
|
|
.color,
|
|
Colors.black,
|
|
);
|
|
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
|
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_gate_comments_before_opening_sheet_when_signed_out', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('player-rail-comments')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Sign in to continue'), findsOneWidget);
|
|
expect(find.byKey(const Key('player-comments-sheet')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_open_feed_comments_when_signed_in', (tester) async {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.withData({
|
|
'auth.accessToken': 'access',
|
|
'auth.refreshToken': 'refresh',
|
|
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
|
|
});
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-rail-comments')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-comments-sheet')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_refresh_comment_count_after_comment_sheet_closes', (
|
|
tester,
|
|
) async {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.withData({
|
|
'auth.accessToken': 'access',
|
|
'auth.refreshToken': 'refresh',
|
|
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
|
|
});
|
|
final comments = FakeCommentRepository(countResult: 97);
|
|
await _pumpApp(tester, commentRepository: comments);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(
|
|
tester
|
|
.widget<Text>(find.byKey(const Key('player-rail-comments-count')))
|
|
.data,
|
|
'96',
|
|
);
|
|
|
|
await tester.tap(find.byKey(const Key('player-rail-comments')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('player-comments-sheet')), findsOneWidget);
|
|
expect(find.text('97 comments'), findsOneWidget);
|
|
|
|
await tester.binding.handlePopRoute();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(comments.countCalls, ['city-lights-demo', 'city-lights-demo']);
|
|
expect(
|
|
tester
|
|
.widget<Text>(find.byKey(const Key('player-rail-comments-count')))
|
|
.data,
|
|
'97',
|
|
);
|
|
});
|
|
|
|
testWidgets('should_show_center_toggle_only_when_paused', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(find.byKey(const Key('player-play-toggle')), findsOneWidget);
|
|
expect(
|
|
tester
|
|
.widget<PlayerCenterToggle>(
|
|
find.byKey(const Key('player-play-toggle')),
|
|
)
|
|
.playing,
|
|
isFalse,
|
|
);
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-play-toggle')),
|
|
matching: find.byIcon(Icons.play_arrow_rounded),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
|
|
await tester.tap(find.byKey(const Key('player-play-toggle')));
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_double_tap_like_once_without_pausing_full_player', (
|
|
tester,
|
|
) async {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.withData({
|
|
'auth.accessToken': 'test-access',
|
|
'auth.refreshToken': 'test-refresh',
|
|
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
|
});
|
|
final interactions = FakeInteractionRepository(
|
|
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
|
);
|
|
await _pumpApp(tester, interactions: interactions);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
|
final gesture = find.byKey(const Key('player-video-tap')).hitTestable();
|
|
final position = tester.getCenter(gesture);
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
|
|
for (var index = 0; index < 4; index++) {
|
|
await tester.tapAt(position);
|
|
}
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
expect(interactions.likeTargets, ['EPISODE:city-lights-demo-1']);
|
|
for (var index = 0; index < 3; index++) {
|
|
expect(find.byKey(Key('double-tap-heart-$index')), findsOneWidget);
|
|
}
|
|
expect(
|
|
tester
|
|
.widget<Image>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-like')),
|
|
matching: find.byType(Image),
|
|
),
|
|
)
|
|
.color,
|
|
AppColors.doubleTapHeartRed,
|
|
);
|
|
|
|
await tester.pump(const Duration(milliseconds: 1600));
|
|
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
});
|
|
|
|
testWidgets('should_double_tap_like_in_clear_screen', (tester) async {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.withData({
|
|
'auth.accessToken': 'test-access',
|
|
'auth.refreshToken': 'test-refresh',
|
|
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
|
});
|
|
final interactions = FakeInteractionRepository();
|
|
await _pumpApp(tester, interactions: interactions);
|
|
await _openSeriesPlayer(tester);
|
|
final logic = _pageLogic(tester, find.byType(PlayerPage).last);
|
|
|
|
await _clearFromMore(tester);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(logic.session.userWantsPlay, isTrue);
|
|
expect(interactions.likeTargets, hasLength(1));
|
|
expect(find.byKey(const Key('double-tap-heart-0')), findsOneWidget);
|
|
expect(find.byKey(const Key('double-tap-heart-1')), findsOneWidget);
|
|
expect(find.byKey(const Key('double-tap-heart-2')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_keep_controls_visible_until_clear_screen_is_tapped', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(find.byType(PlayerCenterToggle), findsNothing);
|
|
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
|
expect(
|
|
tester
|
|
.widget<PlayerCenterToggle>(
|
|
find.byKey(const Key('player-play-toggle')),
|
|
)
|
|
.playing,
|
|
isFalse,
|
|
);
|
|
|
|
await tester.pump(const Duration(seconds: 5));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
|
|
|
await _clearFromMore(tester);
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
|
expect(
|
|
tester
|
|
.widget<PlayerCenterToggle>(
|
|
find.byKey(const Key('player-play-toggle')),
|
|
)
|
|
.playing,
|
|
isFalse,
|
|
);
|
|
expect(find.byKey(const Key('player-video-tap')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
expect(find.byTooltip('Exit clear screen'), findsOneWidget);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))),
|
|
const Size(40, 40),
|
|
);
|
|
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
|
expect(find.byType(PlayerCenterToggle), findsNothing);
|
|
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
expect(find.byType(PlayerCenterToggle), findsOneWidget);
|
|
expect(find.byTooltip('Clear screen'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_open_more_sheet_on_center_long_press', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
|
|
final gesture = await tester.startGesture(const Offset(195, 300));
|
|
await tester.pump(const Duration(milliseconds: 700));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-more-sheet')), findsOneWidget);
|
|
await tester.tapAt(const Offset(10, 10));
|
|
await tester.pumpAndSettle();
|
|
// 长按弹更多不改播放状态,仍处于播放中。
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('should_boost_speed_on_side_long_press_and_restore', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
|
|
final gesture = await tester.startGesture(const Offset(60, 300));
|
|
await tester.pump(const Duration(milliseconds: 700));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-episodes-pill')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsNothing);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-speed-entry')),
|
|
matching: find.text('2.0x'),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
// 加速与清屏都不改变播放状态。
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsNothing);
|
|
expect(find.byKey(const Key('player-episodes-pill')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-speed-entry')),
|
|
matching: find.text('1.0x'),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
|
|
// 右侧 1/3 同样触发加速。
|
|
final rightSide = await tester.startGesture(const Offset(330, 300));
|
|
await tester.pump(const Duration(milliseconds: 700));
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsOneWidget);
|
|
await rightSide.up();
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsNothing);
|
|
});
|
|
|
|
testWidgets('should_keep_previous_clear_state_after_boost', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
|
|
final gesture = await tester.startGesture(const Offset(60, 300));
|
|
await tester.pump(const Duration(milliseconds: 700));
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-episodes-pill')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsNothing);
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
// 本来已清屏,松手后仍保持清屏,只恢复倍速与提示。
|
|
expect(find.byKey(const Key('player-speed-boost-hint')), findsNothing);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byTooltip('Exit clear screen'), findsOneWidget);
|
|
// 清屏态本来显示的选集胶囊与右下按钮,在加速结束后恢复。
|
|
expect(find.byKey(const Key('player-episodes-pill')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-speed-entry')),
|
|
matching: find.text('1.0x'),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('should_fit_player_header_on_320_width', (tester) async {
|
|
await _pumpApp(tester, size: const Size(320, 568));
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-back'))),
|
|
const Size(36, 36),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('should_keep_compact_bottom_panel_and_lower_rail', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
final screenHeight = tester.view.physicalSize.height;
|
|
final bottom = tester.getRect(
|
|
find.byKey(const Key('player-controls-bottom')),
|
|
);
|
|
final rail = tester.getRect(
|
|
find.byKey(const Key('player-interaction-rail')),
|
|
);
|
|
expect(find.byKey(const Key('player-background')), findsNothing);
|
|
final fallback = find.byKey(const Key('player-fallback-background'));
|
|
expect(fallback, findsOneWidget);
|
|
expect(tester.widget<ColoredBox>(fallback).color, Colors.black);
|
|
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
|
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
|
expect(
|
|
tester
|
|
.widget<Scaffold>(find.byKey(const Key('player-scaffold')))
|
|
.backgroundColor,
|
|
Colors.transparent,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Container>(find.byKey(const Key('player-controls-top')))
|
|
.decoration,
|
|
isNull,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Padding>(find.byKey(const Key('player-controls-bottom')))
|
|
.child,
|
|
isNotNull,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Material>(find.byKey(const Key('player-episodes-pill')))
|
|
.color,
|
|
AppColors.playerOverlayPill,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Material>(find.byKey(const Key('player-episodes-pill')))
|
|
.borderRadius,
|
|
BorderRadius.circular(7),
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find
|
|
.descendant(
|
|
of: find.byKey(const Key('player-episodes-pill')),
|
|
matching: find.byType(Text),
|
|
)
|
|
.first,
|
|
)
|
|
.style
|
|
?.fontSize,
|
|
13.5,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-bottom-clear-screen'))),
|
|
const Size(40, 40),
|
|
);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
|
|
expect(find.byType(Slider), findsNothing);
|
|
|
|
expect(bottom.height, lessThanOrEqualTo(200));
|
|
expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78));
|
|
expect(rail.top, greaterThanOrEqualTo(screenHeight * 0.40));
|
|
expect(rail.bottom, lessThanOrEqualTo(bottom.top));
|
|
expect(find.byKey(const Key('player-author-note')), findsNothing);
|
|
expect(
|
|
tester
|
|
.getSize(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-comments')),
|
|
matching: find.byType(Image),
|
|
),
|
|
)
|
|
.width,
|
|
32,
|
|
);
|
|
|
|
final bottomPanel = tester.getRect(
|
|
find.byKey(const Key('player-controls-bottom')),
|
|
);
|
|
expect(bottomPanel.height, lessThanOrEqualTo(330));
|
|
expect(bottomPanel.top, greaterThanOrEqualTo(screenHeight * 0.5));
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets(
|
|
'should_keep_fullscreen_player_position_when_keyboard_insets_change',
|
|
(tester) async {
|
|
addTearDown(tester.view.resetViewInsets);
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
final rail = find.byKey(const Key('player-interaction-rail'));
|
|
final before = tester.getRect(rail);
|
|
|
|
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
|
await tester.pump();
|
|
|
|
expect(
|
|
tester
|
|
.widget<Scaffold>(find.byKey(const Key('player-scaffold')))
|
|
.resizeToAvoidBottomInset,
|
|
isFalse,
|
|
);
|
|
expect(tester.getRect(rail), before);
|
|
},
|
|
);
|
|
|
|
testWidgets('should_keep_series_clear_state_when_swiping_next_episode', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
await _clearFromMore(tester);
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
|
|
await tester.drag(find.byType(PageView), const Offset(0, -550));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_push_episodes_arrow_to_pill_right', (tester) async {
|
|
// 宽屏下文案不会撑满胶囊,才能验证箭头是否被推到最右侧。
|
|
await _pumpApp(tester, size: const Size(900, 844));
|
|
await _openSeriesPlayer(tester);
|
|
|
|
final pill = tester.getRect(find.byKey(const Key('player-episodes-pill')));
|
|
final label = tester.getRect(
|
|
find
|
|
.descendant(
|
|
of: find.byKey(const Key('player-episodes-pill')),
|
|
matching: find.byType(Text),
|
|
)
|
|
.first,
|
|
);
|
|
final arrow = tester.getRect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-episodes-pill')),
|
|
matching: find.byIcon(Icons.keyboard_arrow_up_rounded),
|
|
),
|
|
);
|
|
expect(pill.right - arrow.right, closeTo(14, 0.5));
|
|
expect(arrow.left - label.right, closeTo(8, 0.5));
|
|
});
|
|
|
|
testWidgets('should_show_rail_counts_from_detail_and_follow_server_results', (
|
|
tester,
|
|
) async {
|
|
SharedPreferencesAsyncPlatform.instance =
|
|
InMemorySharedPreferencesAsync.withData({
|
|
'auth.accessToken': 'test-access',
|
|
'auth.refreshToken': 'test-refresh',
|
|
'auth.user': '{"uid":"9","nickname":"Me","avatarUrl":""}',
|
|
});
|
|
final interactions = FakeInteractionRepository(
|
|
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
|
|
);
|
|
await _pumpApp(tester, interactions: interactions);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
String railLabel(Key key) => tester
|
|
.widget<Text>(
|
|
find.descendant(of: find.byKey(key), matching: find.byType(Text)),
|
|
)
|
|
.data!;
|
|
expect(railLabel(const Key('player-rail-like')), '1.2万');
|
|
expect(railLabel(const Key('player-rail-save')), '3200');
|
|
expect(railLabel(const Key('player-rail-comments')), '96');
|
|
// 分享没有数量,仍是文字标签。
|
|
expect(railLabel(const Key('player-rail-share')), 'Share');
|
|
|
|
await tester.tap(find.byKey(const Key('player-rail-like')));
|
|
await tester.pumpAndSettle();
|
|
expect(railLabel(const Key('player-rail-like')), '2.3万');
|
|
expect(interactions.likeTargets, contains('EPISODE:city-lights-demo-1'));
|
|
|
|
await tester.tap(find.byKey(const Key('player-rail-save')));
|
|
await tester.pumpAndSettle();
|
|
expect(railLabel(const Key('player-rail-save')), '3201');
|
|
});
|
|
|
|
testWidgets('should_render_reference_rail_icons_without_background', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(
|
|
_railAssetName(tester, const Key('player-rail-save')),
|
|
'assets/icon/rail/rail_save.png',
|
|
);
|
|
expect(
|
|
_railAssetName(tester, const Key('player-rail-comments')),
|
|
'assets/icon/rail/rail_comment.png',
|
|
);
|
|
expect(
|
|
_railAssetName(tester, const Key('player-rail-like')),
|
|
'assets/icon/rail/rail_like.png',
|
|
);
|
|
expect(
|
|
_railAssetName(tester, const Key('player-rail-share')),
|
|
'assets/icon/rail/rail_share.png',
|
|
);
|
|
final shareImage = tester.widget<Image>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-share')),
|
|
matching: find.byType(Image),
|
|
),
|
|
);
|
|
expect(shareImage.width, 32);
|
|
expect(shareImage.height, 32);
|
|
expect(shareImage.colorBlendMode, BlendMode.srcIn);
|
|
|
|
final saveButton = tester.widget<IconButton>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-save')),
|
|
matching: find.byType(IconButton),
|
|
),
|
|
);
|
|
expect(saveButton.style?.backgroundColor, isNull);
|
|
expect(
|
|
tester.getSize(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-save')),
|
|
matching: find.byType(IconButton),
|
|
),
|
|
),
|
|
const Size(44, 44),
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-rail-like')),
|
|
matching: find.byType(Text),
|
|
),
|
|
)
|
|
.style
|
|
?.fontSize,
|
|
12,
|
|
);
|
|
});
|
|
|
|
testWidgets('should_toggle_synopsis_with_inline_text_button', (tester) async {
|
|
final longCopy = Drama(
|
|
id: 'long-copy-demo',
|
|
title: 'Long Title',
|
|
synopsis:
|
|
'A very long synopsis that keeps describing the story far beyond '
|
|
'the two lines available under the title in the fullscreen player.',
|
|
posterUrl: '/files/long-copy-demo.png',
|
|
genre: 'Drama',
|
|
detailLoaded: true,
|
|
episodes: const [
|
|
Episode(
|
|
id: 'long-copy-demo-1',
|
|
number: 1,
|
|
title: 'Episode 1',
|
|
isFree: true,
|
|
videoUrl: 'https://example.test/long-demo-1.mp4',
|
|
),
|
|
],
|
|
);
|
|
await _pumpApp(tester, catalog: FakeCatalogRepository(dramas: [longCopy]));
|
|
await _openSeriesPlayer(
|
|
tester,
|
|
cardKey: const Key('home-feed-card-long-copy-demo'),
|
|
);
|
|
|
|
final toggle = find.byKey(const Key('player-synopsis-toggle'));
|
|
final synopsis = find.byKey(const Key('player-synopsis'));
|
|
expect(
|
|
find.descendant(of: toggle, matching: find.text('More')),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find.descendant(of: toggle, matching: find.text('More')),
|
|
)
|
|
.style
|
|
?.color,
|
|
AppColors.textPrimary,
|
|
);
|
|
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
|
expect(
|
|
tester
|
|
.widget<Padding>(find.byKey(const Key('player-synopsis-gap')))
|
|
.padding,
|
|
const EdgeInsets.only(left: 6),
|
|
);
|
|
final synopsisRect = tester.getRect(synopsis);
|
|
final toggleRect = tester.getRect(toggle);
|
|
expect(toggleRect.left, greaterThan(synopsisRect.left));
|
|
expect(
|
|
toggleRect.center.dy,
|
|
greaterThan(synopsisRect.top + synopsisRect.height * 0.45),
|
|
);
|
|
|
|
await tester.tap(toggle);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.descendant(of: toggle, matching: find.text('Less')),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
tester.widget<Text>(find.byKey(const Key('player-synopsis'))).maxLines,
|
|
isNull,
|
|
);
|
|
final lessRect = tester.getRect(toggle);
|
|
final textRect = tester.getRect(find.byKey(const Key('player-synopsis')));
|
|
final block = tester.getRect(
|
|
find.byKey(const Key('player-episode-synopsis')),
|
|
);
|
|
expect(lessRect.top, greaterThanOrEqualTo(textRect.bottom - 1));
|
|
expect(block.right - lessRect.right, lessThanOrEqualTo(2));
|
|
|
|
await tester.tap(toggle);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.descendant(of: toggle, matching: find.text('More')),
|
|
findsOneWidget,
|
|
);
|
|
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
|
});
|
|
|
|
testWidgets('should_share_one_muted_style_across_label_and_synopsis', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
TextStyle styleOf(Finder finder) =>
|
|
tester.widget<Text>(finder).style ?? const TextStyle();
|
|
final label = styleOf(find.byKey(const Key('player-episode-label')));
|
|
final synopsis = styleOf(find.byKey(const Key('player-synopsis')));
|
|
final separator = styleOf(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-episode-synopsis')),
|
|
matching: find.text('|'),
|
|
),
|
|
);
|
|
for (final style in [label, synopsis, separator]) {
|
|
expect(style.fontSize, 12);
|
|
expect(style.fontWeight, FontWeight.w500);
|
|
expect(
|
|
style.color?.withValues(alpha: 1),
|
|
AppColors.playerMuted.withValues(alpha: 1),
|
|
);
|
|
}
|
|
expect(separator.color, isNot(Colors.white));
|
|
});
|
|
|
|
testWidgets('should_fit_long_title_and_synopsis_at_320_width', (
|
|
tester,
|
|
) async {
|
|
final longCopy = Drama(
|
|
id: 'long-copy-demo',
|
|
title:
|
|
'An Extremely Long Drama Title That Keeps Going Beyond Any '
|
|
'Sensible Width On A Narrow Screen',
|
|
synopsis:
|
|
'A very long synopsis that keeps describing the story far beyond '
|
|
'the single line space available in the compact bottom panel of '
|
|
'the fullscreen player.',
|
|
posterUrl: '/files/long-copy-demo.png',
|
|
genre: 'Drama',
|
|
detailLoaded: true,
|
|
episodes: const [
|
|
Episode(
|
|
id: 'long-copy-demo-1',
|
|
number: 1,
|
|
title: 'Episode 1',
|
|
isFree: true,
|
|
videoUrl: 'https://example.test/long-demo-1.mp4',
|
|
),
|
|
],
|
|
);
|
|
await _pumpApp(
|
|
tester,
|
|
size: const Size(320, 568),
|
|
catalog: FakeCatalogRepository(dramas: [longCopy]),
|
|
);
|
|
await _openSeriesPlayer(
|
|
tester,
|
|
cardKey: const Key('home-feed-card-long-copy-demo'),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
final title = tester.getRect(find.byKey(const Key('player-drama-title')));
|
|
final chevron = tester.getRect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-open-detail')),
|
|
matching: find.byIcon(Icons.chevron_right_rounded),
|
|
),
|
|
);
|
|
expect(chevron.left - title.right, lessThanOrEqualTo(6));
|
|
expect(chevron.right, lessThanOrEqualTo(320 - 14));
|
|
final synopsis = tester.widget<Text>(
|
|
find.byKey(const Key('player-synopsis')),
|
|
);
|
|
expect(
|
|
tester.getRect(find.byKey(const Key('player-synopsis'))).right,
|
|
lessThanOrEqualTo(320 - 14 - 54),
|
|
);
|
|
expect(synopsis.maxLines, 2);
|
|
expect(synopsis.overflow, TextOverflow.ellipsis);
|
|
expect(
|
|
tester.getRect(find.byKey(const Key('player-episodes-pill'))).right,
|
|
lessThanOrEqualTo(320 - 14 - 40 - 8),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('should_open_speed_sheet_and_apply_selected_speed', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('player-speed-entry')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-speed-sheet')), findsOneWidget);
|
|
for (final value in ['0.75', '1.0', '1.25', '1.5', '2.0', '3.0']) {
|
|
expect(find.byKey(Key('player-speed-$value')), findsOneWidget);
|
|
}
|
|
|
|
await tester.tap(find.byKey(const Key('player-speed-1.5')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-speed-sheet')), findsNothing);
|
|
await tester.tap(find.byKey(const Key('player-speed-entry')));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-speed-entry')),
|
|
matching: find.text('1.5x'),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('should_hide_speed_entry_in_clear_screen_and_keep_speed', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
await tester.tap(find.byKey(const Key('player-speed-entry')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-speed-1.5')));
|
|
await tester.pumpAndSettle();
|
|
|
|
await _clearFromMore(tester);
|
|
await tester.pump();
|
|
expect(find.byKey(const Key('player-more')), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pump();
|
|
await tester.drag(find.byType(PageView), const Offset(0, -550));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-speed-entry')),
|
|
matching: find.text('1.5x'),
|
|
),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('should_enable_drag_time_preview_on_player_progress', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
final bar = tester.widget<SeekProgressBar>(
|
|
find.byKey(const Key('player-progress')),
|
|
);
|
|
expect(bar.showTimes, isTrue);
|
|
});
|
|
|
|
testWidgets('should_render_k28_player_structure_and_episode_grid', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
_expectFullscreenOverlay(tester);
|
|
await tester.tap(find.byKey(const Key('player-episodes')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-episodes-sheet')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-episodes-close')), findsOneWidget);
|
|
final grid = tester.widget<GridView>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-episodes-sheet')),
|
|
matching: find.byType(GridView),
|
|
),
|
|
);
|
|
final delegate =
|
|
grid.gridDelegate as SliverGridDelegateWithFixedCrossAxisCount;
|
|
expect(delegate.crossAxisCount, 6);
|
|
final overlay = tester.widget<AnnotatedRegion<SystemUiOverlayStyle>>(
|
|
find.byKey(const Key('player-episodes-system-overlay')),
|
|
);
|
|
expect(overlay.value.systemNavigationBarColor, AppColors.card);
|
|
expect(overlay.value.systemNavigationBarContrastEnforced, isFalse);
|
|
final safeArea = tester.widget<SafeArea>(
|
|
find
|
|
.descendant(
|
|
of: find.byKey(const Key('player-episodes-sheet')),
|
|
matching: find.byType(SafeArea),
|
|
)
|
|
.first,
|
|
);
|
|
expect(safeArea.bottom, isTrue);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('should_show_locked_episode_in_episode_picker', (tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
catalog: FakeCatalogRepository(dramas: const [_lockedDrama]),
|
|
playback: FakePlaybackRepository(
|
|
ticket: const PlayTicket(
|
|
episodeId: 'locked-demo-1',
|
|
accessible: false,
|
|
accessType: PlayTicket.coinRequired,
|
|
unlockCoin: 30,
|
|
posterUrl: '/files/locked-first-frame.jpg',
|
|
thumbnailsUrl: '/files/locked-thumbnails.vtt',
|
|
),
|
|
),
|
|
);
|
|
await _openSeriesPlayer(
|
|
tester,
|
|
cardKey: const Key('home-feed-card-locked-demo'),
|
|
);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-loading-highlight')), findsNothing);
|
|
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
|
|
expect(find.byType(Slider), findsNothing);
|
|
expect(find.byKey(const Key('player-single-placeholder')), findsNothing);
|
|
expect(find.byKey(const Key('player-opening-poster')), findsNothing);
|
|
expect(find.byKey(const Key('player-opening-sprite')), findsNothing);
|
|
expect(find.byKey(const Key('player-video-crossfade')), findsNothing);
|
|
expect(find.byKey(const Key('player-video-layer')), findsNothing);
|
|
expect(
|
|
tester
|
|
.widget<SeekProgressBar>(find.byKey(const Key('player-progress')))
|
|
.locked,
|
|
isTrue,
|
|
);
|
|
await tester.tap(find.byKey(const Key('player-episodes')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.byKey(const ValueKey('episode-lock-locked-demo-1')),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byKey(const Key('player-episode-2')), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_open_compact_more_actions', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-more-sheet')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-speed')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-drag-handle')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-close')), findsNothing);
|
|
for (final removed in const [
|
|
Key('player-more-not-interested'),
|
|
Key('player-more-danmaku'),
|
|
Key('player-more-danmaku-settings'),
|
|
Key('player-more-text-size'),
|
|
Key('player-more-cast'),
|
|
Key('player-more-timer'),
|
|
Key('player-more-report'),
|
|
]) {
|
|
expect(find.byKey(removed), findsNothing);
|
|
}
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-speed-1.0'))).height,
|
|
26,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-speed-track'))).height,
|
|
34,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-speed-row'))).height,
|
|
56,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-quality-row'))).height,
|
|
56,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-clear'))).height,
|
|
56,
|
|
);
|
|
expect(
|
|
tester.getSize(find.byKey(const Key('player-more-offline'))).height,
|
|
56,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<SingleChildScrollView>(
|
|
find.byKey(const Key('player-more-speed')),
|
|
)
|
|
.scrollDirection,
|
|
Axis.horizontal,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<SingleChildScrollView>(
|
|
find.byKey(const Key('player-more-quality')),
|
|
)
|
|
.scrollDirection,
|
|
Axis.horizontal,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-more-speed-1.0')),
|
|
matching: find.text('1.0x'),
|
|
),
|
|
)
|
|
.style
|
|
?.fontSize,
|
|
13,
|
|
);
|
|
await tester.ensureVisible(find.byKey(const Key('player-more-speed-1.5')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-more-speed-1.5')));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-more-speed-1.5')),
|
|
matching: find.text('1.5x'),
|
|
),
|
|
)
|
|
.style
|
|
?.fontWeight,
|
|
FontWeight.w600,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<Text>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-more-speed-1.0')),
|
|
matching: find.text('1.0x'),
|
|
),
|
|
)
|
|
.style
|
|
?.fontWeight,
|
|
FontWeight.w400,
|
|
);
|
|
});
|
|
|
|
testWidgets('should_disable_quality_not_supplied_by_ticket', (tester) async {
|
|
await _pumpApp(
|
|
tester,
|
|
playback: FakePlaybackRepository(
|
|
ticket: const PlayTicket(
|
|
episodeId: 'city-lights-demo-1',
|
|
accessible: true,
|
|
accessType: PlayTicket.free,
|
|
playUrl: '/files/city-lights-demo-1.mp4',
|
|
qualities: ['AUTO'],
|
|
),
|
|
),
|
|
);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester
|
|
.widget<InkWell>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-more-quality-AUTO')),
|
|
matching: find.byType(InkWell),
|
|
),
|
|
)
|
|
.onTap,
|
|
isNotNull,
|
|
);
|
|
expect(
|
|
tester
|
|
.widget<InkWell>(
|
|
find.descendant(
|
|
of: find.byKey(const Key('player-more-quality-720P')),
|
|
matching: find.byType(InkWell),
|
|
),
|
|
)
|
|
.onTap,
|
|
isNull,
|
|
);
|
|
expect(find.text('720P · Unavailable'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_show_pip_action_when_platform_supported', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-more-pip')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
|
|
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
|
|
|
testWidgets('should_hide_pip_action_when_platform_unsupported', (
|
|
tester,
|
|
) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-more-pip')), findsNothing);
|
|
expect(find.byKey(const Key('player-more-clear')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
|
|
}, variant: TargetPlatformVariant.only(TargetPlatform.macOS));
|
|
|
|
testWidgets('should_enter_clear_screen_from_more_panel', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-more-clear')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byTooltip('Exit clear screen'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_enter_clear_screen_from_bottom_button', (tester) async {
|
|
await _pumpApp(tester);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-pip')), findsNothing);
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsNothing);
|
|
expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-progress')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsNothing);
|
|
expect(find.byType(PlayerCenterToggle), findsNothing);
|
|
expect(find.byTooltip('Exit clear screen'), findsOneWidget);
|
|
|
|
await tester.tap(find.byKey(const Key('player-bottom-clear-screen')));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('player-controls-bottom')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
expect(find.byType(PlayerCenterToggle), findsNothing);
|
|
expect(find.byTooltip('Clear screen'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('should_open_unlock_panel_with_honest_method_states', (
|
|
tester,
|
|
) async {
|
|
final interactions = FakeInteractionRepository();
|
|
await _pumpApp(
|
|
tester,
|
|
interactions: interactions,
|
|
playback: FakePlaybackRepository(
|
|
ticket: const PlayTicket(
|
|
episodeId: 'city-lights-demo-1',
|
|
accessible: false,
|
|
accessType: PlayTicket.coinRequired,
|
|
unlockCoin: 30,
|
|
),
|
|
),
|
|
);
|
|
await _openSeriesPlayer(tester);
|
|
|
|
expect(find.byKey(const Key('player-unlock-notice')), findsOneWidget);
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.tapAt(const Offset(80, 300));
|
|
await tester.pump(const Duration(milliseconds: 350));
|
|
expect(find.byKey(const Key('double-tap-heart-0')), findsNothing);
|
|
expect(interactions.likeTargets, isEmpty);
|
|
|
|
await tester.tap(find.byKey(const Key('player-open-unlock')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('player-unlock-panel')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-unlock-coins')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-unlock-ad')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-unlock-vip')), findsOneWidget);
|
|
expect(
|
|
find.text('Available unlock methods vary by title.'),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
}
|
|
|
|
String _railAssetName(WidgetTester tester, Key itemKey) {
|
|
final image = tester.widget<Image>(
|
|
find.descendant(of: find.byKey(itemKey), matching: find.byType(Image)),
|
|
);
|
|
return (image.image as AssetImage).assetName;
|
|
}
|
|
|
|
void _expectPlayerNavigationBar(WidgetTester tester) {
|
|
final overlay = tester.widget<AnnotatedRegion<SystemUiOverlayStyle>>(
|
|
find.byKey(const Key('player-system-overlay')).last,
|
|
);
|
|
expect(overlay.value.systemNavigationBarColor, Colors.black);
|
|
expect(overlay.value.systemNavigationBarDividerColor, Colors.transparent);
|
|
expect(overlay.value.systemNavigationBarContrastEnforced, isFalse);
|
|
expect(overlay.value.systemNavigationBarIconBrightness, Brightness.light);
|
|
}
|
|
|
|
void _expectFullscreenOverlay(WidgetTester tester) {
|
|
expect(find.byKey(const Key('player-back')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-episode-heading')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-speed-entry')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-drama-title')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-episode-label')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-synopsis')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-more')), findsOneWidget);
|
|
for (final key in const [Key('player-back'), Key('player-more')]) {
|
|
expect(tester.getSize(find.byKey(key)), const Size(36, 36));
|
|
}
|
|
expect(find.byKey(const Key('player-play-toggle')), findsNothing);
|
|
expect(find.byKey(const Key('player-episodes')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-bottom-clear-screen')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-pip')), findsNothing);
|
|
expect(find.byKey(const Key('player-rail-comments')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-fullscreen-toggle')), findsNothing);
|
|
expect(find.byKey(const Key('progress-loading')), findsNothing);
|
|
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
|
|
expect(find.byType(Slider), findsNothing);
|
|
expect(find.byKey(const Key('player-title')), findsNothing);
|
|
expect(find.byKey(const Key('player-subtitle')), findsNothing);
|
|
expect(find.byKey(const Key('player-genre-chip')), findsNothing);
|
|
expect(find.byKey(const Key('player-page-dots')), findsNothing);
|
|
expect(find.byKey(const Key('player-clear-screen')), findsNothing);
|
|
expect(find.byKey(const Key('player-next')), findsNothing);
|
|
|
|
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-rail-like')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-rail-save')), findsOneWidget);
|
|
expect(find.byKey(const Key('player-rail-share')), findsOneWidget);
|
|
}
|
|
|
|
Future<void> _clearFromMore(WidgetTester tester) async {
|
|
await tester.tap(find.byKey(const Key('player-more')));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byKey(const Key('player-more-clear')));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> _openSeriesPlayer(
|
|
WidgetTester tester, {
|
|
Key cardKey = const Key('home-feed-card-city-lights-demo'),
|
|
}) async {
|
|
await tester.tap(find.byKey(const Key('tab-home')));
|
|
await tester.pumpAndSettle();
|
|
final card = find.byKey(cardKey);
|
|
await tester.scrollUntilVisible(
|
|
card,
|
|
220,
|
|
scrollable: find
|
|
.descendant(
|
|
of: find.byKey(const Key('home-grid-refresh')),
|
|
matching: find.byType(Scrollable),
|
|
)
|
|
.first,
|
|
);
|
|
await tester.ensureVisible(card);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(card);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> _pumpApp(
|
|
WidgetTester tester, {
|
|
Size size = const Size(390, 844),
|
|
FakeCatalogRepository? catalog,
|
|
PlaybackRepository? playback,
|
|
FakeInteractionRepository? interactions,
|
|
FakeCommentRepository? commentRepository,
|
|
PlayerFactory? playerFactory,
|
|
bool settle = true,
|
|
}) async {
|
|
tester.view.physicalSize = size;
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
final repository = catalog ?? FakeCatalogRepository();
|
|
await tester.pumpWidget(
|
|
LumaDramaApp(
|
|
playerFactory: playerFactory ?? createWidgetPlayer,
|
|
catalogRepository: repository,
|
|
categoryRepository: FakeCategoryRepository(catalog: repository),
|
|
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
|
favoriteRepository: InMemoryFavoriteRepository(cards: repository.dramas),
|
|
playbackRepository: playback ?? FakePlaybackRepository(),
|
|
commentRepository: commentRepository ?? FakeCommentRepository(),
|
|
interactionRepository: interactions ?? FakeInteractionRepository(),
|
|
noticeRepository: FakeNoticeRepository(),
|
|
haptics: FakeHaptics(),
|
|
),
|
|
);
|
|
if (settle) await tester.pumpAndSettle();
|
|
}
|
|
|
|
PlayerLogic _pageLogic(WidgetTester tester, Finder page) {
|
|
final dynamic state = tester.state(page);
|
|
return state.logic as PlayerLogic;
|
|
}
|
|
|
|
Finder _fullPlayerFinder() {
|
|
return find.byWidgetPredicate(
|
|
(widget) => widget is PlayerPage && widget.handoff != null,
|
|
);
|
|
}
|
|
|
|
class _RouteVideoFactory {
|
|
final List<_RouteVideoPlayer> created = [];
|
|
final List<Episode> episodes = [];
|
|
|
|
PlayerPort create(Episode episode) {
|
|
final player = _RouteVideoPlayer(episode);
|
|
created.add(player);
|
|
episodes.add(episode);
|
|
return player;
|
|
}
|
|
}
|
|
|
|
class _RouteVideoPlayer extends VideoPlayerPort {
|
|
_RouteVideoPlayer(super.episode);
|
|
|
|
bool disposed = false;
|
|
int seekCalls = 0;
|
|
int playCalls = 0;
|
|
|
|
bool get playing => controller.value.isPlaying;
|
|
|
|
@override
|
|
Future<void> initialize() async {
|
|
controller.value = const VideoPlayerValue(
|
|
duration: Duration(minutes: 1),
|
|
size: Size(1280, 720),
|
|
position: Duration(seconds: 5),
|
|
isInitialized: true,
|
|
isPlaying: true,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> play() async {
|
|
playCalls++;
|
|
final value = controller.value;
|
|
controller.value = VideoPlayerValue(
|
|
duration: value.duration,
|
|
size: value.size,
|
|
position: value.position,
|
|
isInitialized: value.isInitialized,
|
|
isPlaying: true,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> pause() async {
|
|
final value = controller.value;
|
|
controller.value = VideoPlayerValue(
|
|
duration: value.duration,
|
|
size: value.size,
|
|
position: value.position,
|
|
isInitialized: value.isInitialized,
|
|
isPlaying: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> seekTo(Duration position) async {
|
|
seekCalls++;
|
|
final value = controller.value;
|
|
controller.value = VideoPlayerValue(
|
|
duration: value.duration,
|
|
size: value.size,
|
|
position: position,
|
|
isInitialized: value.isInitialized,
|
|
isPlaying: value.isPlaying,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
disposed = true;
|
|
await super.dispose();
|
|
}
|
|
}
|
|
|
|
class _RecordingPlayer implements PlayerPort {
|
|
bool playing = false;
|
|
|
|
@override
|
|
Future<void> initialize() async {}
|
|
|
|
@override
|
|
Future<void> play() async {
|
|
playing = true;
|
|
}
|
|
|
|
@override
|
|
Future<void> pause() async {
|
|
playing = false;
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {}
|
|
}
|
|
|
|
class _PendingPlaybackRepository implements PlaybackRepository {
|
|
final Completer<PlayTicket> ticket = Completer<PlayTicket>();
|
|
|
|
@override
|
|
Future<PlayTicket> ticketFor(String episodeId, {bool forceRefresh = false}) =>
|
|
ticket.future;
|
|
|
|
@override
|
|
bool isFresh(String episodeId) => false;
|
|
}
|
|
|
|
class _GatedPlayer implements PlayerPort {
|
|
_GatedPlayer(this.initialization);
|
|
|
|
final Future<void> initialization;
|
|
|
|
@override
|
|
Future<void> initialize() => initialization;
|
|
|
|
@override
|
|
Future<void> play() async {}
|
|
|
|
@override
|
|
Future<void> pause() async {}
|
|
|
|
@override
|
|
Future<void> dispose() async {}
|
|
}
|
|
|
|
class _GatedVideoPlayer extends VideoPlayerPort {
|
|
_GatedVideoPlayer(super.episode, this.initialization);
|
|
|
|
final Future<void> initialization;
|
|
|
|
@override
|
|
Future<void> initialize() => initialization;
|
|
}
|
|
|
|
const _lockedDrama = Drama(
|
|
id: 'locked-demo',
|
|
title: 'Locked Demo',
|
|
synopsis: 'Locked demo synopsis',
|
|
posterUrl: '/files/locked-demo.png',
|
|
genre: 'Suspense',
|
|
detailLoaded: true,
|
|
episodes: [
|
|
Episode(
|
|
id: 'locked-demo-1',
|
|
number: 1,
|
|
title: 'Locked Episode',
|
|
isFree: false,
|
|
locked: true,
|
|
videoUrl: 'https://example.test/locked-demo-1.mp4',
|
|
posterUrl: '/files/locked-first-frame.jpg',
|
|
thumbnailsUrl: '/files/locked-thumbnails.vtt',
|
|
),
|
|
Episode(
|
|
id: 'locked-demo-2',
|
|
number: 2,
|
|
title: 'Open Episode',
|
|
isFree: true,
|
|
videoUrl: 'https://example.test/locked-demo-2.mp4',
|
|
),
|
|
],
|
|
);
|