Files
ReelShort/test/player/player_fullscreen_test.dart
T

1390 lines
46 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/theme/app_colors.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.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_open_fullscreen_overlay_from_discover_card', (
tester,
) async {
await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-theater')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('home-drama-card-city-lights-demo')));
await tester.pumpAndSettle();
_expectFullscreenOverlay(tester);
expect(find.byType(PageView), findsOneWidget);
await tester.tap(find.byKey(const Key('player-more')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('player-more-quality-AUTO')), findsOneWidget);
expect(find.byKey(const Key('player-more-offline')), findsOneWidget);
await tester.tapAt(const Offset(10, 10));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
testWidgets('should_use_black_system_navigation_bar_on_feed_and_player', (
tester,
) async {
await _pumpApp(tester);
_expectPlayerNavigationBar(tester);
await _openSeriesPlayer(tester);
_expectPlayerNavigationBar(tester);
});
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);
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);
});
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;
},
);
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 tester.tap(find.byKey(const Key('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_update_home_feed_favorite_count_after_toggle', (
tester,
) async {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.withData({
'auth.accessToken': 'access',
'auth.refreshToken': 'refresh',
'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}',
});
final catalog = FakeCatalogRepository(dramas: [_countingDrama]);
await _pumpApp(tester, catalog: catalog);
expect(
tester.widget<Text>(find.byKey(const Key('rail-save-count'))).data,
'2',
);
await tester.tap(find.byKey(const Key('rail-save')));
await tester.pumpAndSettle();
expect(
tester.widget<Text>(find.byKey(const Key('rail-save-count'))).data,
'3',
);
});
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();
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_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();
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();
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();
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')), 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,
);
await tester.tap(find.byKey(const Key('player-synopsis-toggle')));
await tester.pumpAndSettle();
final expandedBottom = tester.getRect(
find.byKey(const Key('player-controls-bottom')),
);
// K29.9 展开态展示简介全文,高度会增长,但仍需留在画面下半区。
expect(expandedBottom.height, lessThanOrEqualTo(330));
expect(expandedBottom.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":""}',
});
await _pumpApp(
tester,
interactions: FakeInteractionRepository(
nextLikeResult: const LikeToggleResult(liked: true, count: 23000),
),
);
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万');
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 {
await _pumpApp(tester);
await _openSeriesPlayer(tester);
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>(synopsis).maxLines, 1);
expect(
tester.getRect(toggle).left - tester.getRect(synopsis).right,
lessThanOrEqualTo(4),
);
await tester.tap(toggle);
await tester.pumpAndSettle();
expect(
find.descendant(of: toggle, matching: find.text('Less')),
findsOneWidget,
);
expect(tester.widget<Text>(synopsis).maxLines, isNull);
await tester.tap(toggle);
await tester.pumpAndSettle();
expect(
find.descendant(of: toggle, matching: find.text('More')),
findsOneWidget,
);
expect(tester.widget<Text>(synopsis).maxLines, 1);
});
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);
}
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-drama-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(synopsis.maxLines, 1);
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]),
);
await _openSeriesPlayer(
tester,
cardKey: const Key('home-drama-card-locked-demo'),
);
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 {
await _pumpApp(
tester,
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.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')), 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-drama-card-city-lights-demo'),
}) async {
await tester.tap(find.byKey(const Key('tab-theater')));
await tester.pumpAndSettle();
final card = find.byKey(cardKey);
await tester.scrollUntilVisible(
card,
220,
scrollable: find
.descendant(
of: find.byKey(const Key('discover-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,
FakePlaybackRepository? playback,
FakeInteractionRepository? interactions,
FakeCommentRepository? commentRepository,
PlayerFactory? playerFactory,
}) 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(),
),
);
await tester.pumpAndSettle();
}
const _countingDrama = Drama(
id: 'counting-demo',
title: 'Counting Demo',
synopsis: 'Counting interaction totals',
posterUrl: '/files/counting-demo.png',
genre: 'Romance',
detailLoaded: true,
favoriteCount: 2,
commentCount: 3,
likeCount: 4,
episodes: [
Episode(
id: 'counting-demo-1',
number: 1,
title: 'Episode 1',
isFree: true,
videoUrl: 'https://example.test/counting-1.mp4',
),
],
);
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 {}
}
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',
),
Episode(
id: 'locked-demo-2',
number: 2,
title: 'Open Episode',
isFree: true,
videoUrl: 'https://example.test/locked-demo-2.mp4',
),
],
);