// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/comment.dart'; import 'package:fruitshorts/core/data/models/interaction.dart'; import 'package:fruitshorts/pages/player/player_view.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 'support/fake_catalog.dart'; import 'support/fake_category.dart'; import 'support/fake_interaction_repositories.dart'; import 'support/widget_player.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/catalog_repository.dart'; import 'package:fruitshorts/core/data/repositories/comment_repository.dart'; import 'package:fruitshorts/core/data/repositories/interaction_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'; void main() { /// 默认按「已登录」起测:账号体系接上后,"登录"不再是本地演示开关, /// 而是本地会话(这里直接给 shared_preferences 的内存实现塞一份会话)。 /// 昵称/uid 与演示文案保持一致,避免为显示细节改写既有断言。 void seedSignedInSession() { SharedPreferencesAsyncPlatform .instance = InMemorySharedPreferencesAsync.withData({ 'auth.accessToken': 'test-access', 'auth.refreshToken': 'test-refresh', 'auth.user': '{"uid":"M1CZWVYE","nickname":"Vicky sheng","avatarUrl":""}', }); } void seedSignedOutSession() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); } setUp(() { seedSignedInSession(); }); /// 所有 widget 测试都注入离线仓库:票据 / 进度 / 收藏都不碰网络, /// 这样断言的是 UI 行为,而不是后端有没有起。 Widget appUnderTest({ PlayerFactory? playerFactory, CatalogRepository? catalogRepository, CommentRepository? commentRepository, InteractionRepository? interactionRepository, }) { final catalog = catalogRepository ?? FakeCatalogRepository(); return LumaDramaApp( playerFactory: playerFactory ?? createWidgetPlayer, catalogRepository: catalog, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository( cards: FakeCatalogRepository().dramas, ), playbackRepository: FakePlaybackRepository(), commentRepository: commentRepository ?? FakeCommentRepository(), interactionRepository: interactionRepository ?? FakeInteractionRepository(), ); } testWidgets( 'should_promote_next_without_recreating_it_or_showing_loading_when_home_is_swiped', (tester) async { final created = []; await tester.pumpWidget( appUnderTest( playerFactory: (episode) { created.add(episode.id); return WidgetPlayer(); }, ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(created, hasLength(2)); expect(find.byType(CircularProgressIndicator), findsNothing); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsNothing); await tester.pumpAndSettle(); expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(created, hasLength(3)); expect(created.toSet(), hasLength(3)); await tester.pumpWidget(const SizedBox.shrink()); await tester.pumpAndSettle(); }, ); testWidgets('should_open_series_detail_when_immersive_home_is_tapped', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); expect(find.byType(Slider), findsOneWidget); expect(find.text('Home'), findsWidgets); expect(find.textContaining('Playing now'), findsOneWidget); expect(find.text('Recommended'), findsNothing); expect(find.text('Following'), findsNothing); expect(find.byTooltip('Episodes'), findsWidgets); expect(find.byTooltip('Clear screen'), findsOneWidget); expect(find.byTooltip('Picture in picture'), findsNothing); await tester.tap(find.byTooltip('More')); await tester.pumpAndSettle(); expect(find.text('Picture in picture'), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Watch Full'), findsNothing); expect(find.byTooltip('Explore series'), findsNothing); }); testWidgets('should_hide_tabs_and_restore_them_when_clear_screen_is_tapped', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); await tester.tap(find.byTooltip('Clear screen')); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-tabs-bar')), findsNothing); expect(find.byTooltip('Exit clear screen'), findsOneWidget); await tester.tap(find.byTooltip('Exit clear screen')); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); expect(find.byTooltip('Clear screen'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets( 'should_open_current_series_and_switch_root_tabs_when_quick_drawer_is_used', (tester) async { tester.view.physicalSize = const Size(360, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect( tester .widget(find.byType(PlayerPage, skipOffstage: false)) .active, isFalse, ); expect(find.text('Quick access'), findsOneWidget); expect(find.text('Bahasa Indonesia'), findsOneWidget); expect( find.descendant( of: find.byType(Drawer), matching: find.text('7 EPISODES'), ), findsOneWidget, ); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('drawer-open-series'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('detail-cta')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect( tester .widget(find.byType(PlayerPage, skipOffstage: false)) .active, isFalse, ); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('drawer-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect( Navigator.of( tester.element(find.byKey(const Key('home-all-dramas-grid'))), ).canPop(), isFalse, ); }, ); testWidgets('should_change_interface_language_when_locale_is_picked', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); expect(find.byTooltip('Captions'), findsNothing); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); expect(find.text('Vicky sheng'), findsOneWidget); expect(find.text('Coins'), findsOneWidget); await tester.drag(find.byType(ListView), const Offset(0, -700)); await tester.pumpAndSettle(); await tester.tap(find.text('App Language')); await tester.pumpAndSettle(); await tester.tap(find.text('简体中文').last); await tester.pumpAndSettle(); expect(find.text('我的'), findsWidgets); await tester.drag(find.byType(ListView), const Offset(0, 700)); await tester.pumpAndSettle(); expect(find.text('我的账户'), findsOneWidget); expect(find.text('首页'), findsOneWidget); await tester.tap(find.text('找剧')); await tester.pumpAndSettle(); expect(find.text('全部'), findsOneWidget); expect(find.text('热门'), findsOneWidget); }); testWidgets( 'should_fit_compact_android_viewport_when_home_controls_are_shown', (tester) async { tester.view.physicalSize = const Size(360, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(0, -500)); await tester.pumpAndSettle(); expect(find.text('City Lights & Velvet Nights'), findsOneWidget); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); await tester.tap(find.text('City Lights & Velvet Nights')); await tester.pumpAndSettle(); expect(find.byType(Slider), findsOneWidget); expect(tester.takeException(), isNull); }, ); testWidgets( 'should_fit_compact_android_viewport_and_return_home_when_my_page_is_shown', (tester) async { tester.view.physicalSize = const Size(360, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.text('Vicky sheng'), findsOneWidget); expect(find.text('Coins'), findsOneWidget); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); }, ); testWidgets('should_fit_compact_dark_viewport_when_evening_theme_is_used', ( tester, ) async { tester.view.physicalSize = const Size(320, 568); tester.view.devicePixelRatio = 1; tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); expect(find.text('Coins'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets( 'should_show_three_playable_dramas_and_one_pending_poster_when_discover_is_opened', (tester) async { tester.view.physicalSize = const Size(360, 720); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.text('All'), findsOneWidget); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byKey(const Key('discover-profile')), findsOneWidget); expect( find.byKey( const Key('home-drama-genre-city-lights-demo'), skipOffstage: false, ), findsOneWidget, ); expect( find.byKey( const Key('home-drama-genre-vengeance-demo'), skipOffstage: false, ), findsOneWidget, ); expect(find.byTooltip('Search'), findsNothing); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.byKey(const Key('mine-library-tab-1')), 260, ); await tester.tap(find.byKey(const Key('mine-library-tab-1'))); await tester.pumpAndSettle(); expect(find.text('No shows here yet.'), findsOneWidget); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); }, ); testWidgets('should_stay_on_one_route_when_bottom_tabs_are_used', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byTooltip('Search'), findsNothing); expect( Navigator.of( tester.element(find.byKey(const Key('home-all-dramas-grid'))), ).canPop(), isFalse, ); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); await tester.drag(find.byType(ListView), const Offset(0, -700)); await tester.pumpAndSettle(); expect(find.text('History'), findsWidgets); expect( Navigator.of(tester.element(find.text('History').first)).canPop(), isFalse, ); await tester.tap(find.byKey(const Key('mine-library-tab-1'))); await tester.pumpAndSettle(); expect(find.text('No shows here yet.'), findsOneWidget); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect(find.byTooltip('Search'), findsNothing); expect( Navigator.of( tester.element(find.byKey(const Key('home-all-dramas-grid'))), ).canPop(), isFalse, ); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); await tester.drag(find.byType(ListView), const Offset(0, -400)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('mine-library-tab-1'))); await tester.pumpAndSettle(); expect(find.text('No shows here yet.'), findsOneWidget); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); expect( Navigator.of(tester.element(find.text('The Signal'))).canPop(), isFalse, ); }); testWidgets( 'should_change_drama_on_home_swipe_and_episode_on_player_swipe_when_swiping', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(find.textContaining('Episode 1'), findsOneWidget); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); await tester.tap(find.text('City Lights & Velvet Nights')); await tester.pumpAndSettle(); expect(find.byType(Slider), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); expect( tester.widget(find.byKey(const Key('player-title'))).data, contains('Episode 2'), ); expect(find.textContaining('City Lights & Velvet Nights'), findsWidgets); }, ); testWidgets('should_hide_category_navigation_when_shorts_page_is_shown', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.text('Following'), findsNothing); expect(find.text('Animated'), findsNothing); expect(find.text('Live Action'), findsNothing); expect(find.text('Recommended'), findsNothing); expect(find.text('The Signal'), findsOneWidget); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); expect(find.text('City Lights & Velvet Nights'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets( 'should_save_comment_like_and_share_in_order_when_right_rail_is_used', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); String? sharedText; final interactions = FakeInteractionRepository(); final comments = FakeCommentRepository(pages: [const CommentPage()]); const channel = MethodChannel('com.stormg.lumadrama/share'); tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( call, ) async { sharedText = (call.arguments as Map)['text'] as String; return null; }); addTearDown( () => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( channel, null, ), ); await tester.pumpWidget( appUnderTest( commentRepository: comments, interactionRepository: interactions, ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); final keys = [ const Key('rail-like'), const Key('rail-save'), const Key('rail-comments'), const Key('rail-share'), ]; final tops = keys .map((key) => tester.getTopLeft(find.byKey(key)).dy) .toList(); expect(tops, orderedEquals(tops.toList()..sort())); await tester.tap(find.byKey(const Key('rail-save'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('rail-comments'))); await tester.pumpAndSettle(); expect(find.text('No comments yet.'), findsOneWidget); await tester.enterText( find.byKey(const Key('comment-input')), 'Nice demo', ); await tester.tap(find.byKey(const Key('comment-post'))); await tester.pumpAndSettle(); expect(find.text('Nice demo'), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('rail-like'))); await tester.pumpAndSettle(); expect(interactions.likeTargets, contains('DRAMA:the-signal-demo')); await tester.tap(find.byKey(const Key('rail-share'))); await tester.pumpAndSettle(); expect(sharedText, contains('The Signal')); expect(sharedText, contains('A demo series for vertical playback')); expect(sharedText, contains('FruitShorts')); expect(tester.takeException(), isNull); }, ); testWidgets('should_open_auth_gate_without_liking_when_signed_out', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); seedSignedOutSession(); final interactions = FakeInteractionRepository(); await tester.pumpWidget(appUnderTest(interactionRepository: interactions)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('rail-like'))); await tester.pumpAndSettle(); expect(find.text('Sign in to continue'), findsOneWidget); expect(interactions.likeTargets, isEmpty); }); testWidgets('should_report_drama_from_detail_more_menu_when_submitted', ( tester, ) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final interactions = FakeInteractionRepository(); await tester.pumpWidget(appUnderTest(interactionRepository: interactions)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('drawer-open-series'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('detail-more'))); await tester.pumpAndSettle(); await tester.tap(find.text('Report drama')); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('report-submit'))); await tester.pumpAndSettle(); expect(interactions.reports.single.targetType, InteractionTargetType.drama); expect(find.text('Report submitted. Thank you.'), findsOneWidget); }); testWidgets( 'should_open_drawer_and_stay_on_home_when_system_back_is_pressed', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-discover'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Quick access'), findsOneWidget); expect(find.byKey(const Key('home-tabs-bar')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.text('Quick access'), findsNothing); expect(find.byKey(const Key('home-all-dramas-grid')), findsOneWidget); expect( Navigator.of( tester.element(find.byKey(const Key('home-tabs-bar'))), ).canPop(), isFalse, ); }, ); testWidgets( 'should_list_saved_dramas_across_home_feed_when_watchlist_is_opened', (tester) async { tester.view.physicalSize = const Size(390, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); // 收藏要求登录态:沿用本文件已有的"假登录"初始化 seedSignedInSession(); await tester.pumpWidget(appUnderTest()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('rail-save'))); await tester.pumpAndSettle(); await tester.drag(find.byType(PageView), const Offset(0, -550)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('rail-save'))); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-my'))); await tester.pumpAndSettle(); await tester.scrollUntilVisible( find.byKey(const Key('mine-library-tab-1')), 300, ); await tester.tap(find.byKey(const Key('mine-library-tab-1'))); await tester.pumpAndSettle(); expect(find.text('The Signal'), findsOneWidget); expect(find.text('City Lights & Velvet Nights'), findsWidgets); await tester.tap(find.text('The Signal')); await tester.pumpAndSettle(); expect(find.byKey(const Key('detail-cta')), findsOneWidget); }, ); }