// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/feed_page.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/pages/home/home_feed_channel.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_haptics.dart'; import '../support/fake_interaction_repositories.dart'; import '../support/widget_player.dart'; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets('should_render_four_tabs_and_open_create_placeholder', ( tester, ) async { await _pumpApp(tester); for (final key in _tabKeys) { expect(find.byKey(key), findsOneWidget); } await tester.tap(find.byKey(const Key('tab-create'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('create-page')), findsOneWidget); expect(find.text('Create'), findsWidgets); expect(find.text('Stories become short dramas'), findsOneWidget); expect(find.byKey(const Key('create-ai')), findsOneWidget); expect(find.byKey(const Key('create-upload')), findsOneWidget); expect(tester.getSize(find.byKey(const Key('create-ai'))).height, 196); expect(find.byKey(const Key('create-motto-primary')), findsOneWidget); expect(find.byKey(const Key('create-motto-secondary')), findsOneWidget); expect( tester.getSize(find.byKey(const Key('create-motto-line'))), const Size(96, 2), ); await tester.tap(find.byKey(const Key('create-ai'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('create-placeholder-page')), findsOneWidget); expect(find.text('Coming soon'), findsOneWidget); }); testWidgets('should_start_home_on_immersive_feed', (tester) async { await _pumpApp(tester); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-black-background')), findsOneWidget); expect(find.byType(PageView), findsOneWidget); expect(find.byKey(const Key('home-browse-page')), findsNothing); expect( tester.widget(find.byKey(const Key('rail-like-count'))).data, '41.9万', ); expect( tester.widget(find.byKey(const Key('rail-save-count'))).data, '1006', ); expect( tester.widget(find.byKey(const Key('rail-comments-count'))).data, '3.2万', ); expect(find.byKey(const Key('home-feed-refresh')), findsOneWidget); expect(find.byKey(const Key('home-feed-danmaku')), findsNothing); expect(find.byKey(const Key('home-feed-fullscreen')), findsNothing); expect( tester.widget(find.byType(Scaffold).first).backgroundColor, Colors.transparent, ); expect(find.byKey(const Key('home-page-gradient')), findsNothing); expect(_hasLightStatusBar(tester), isTrue); expect(tester.widget(find.byType(PlayerPage)).homeMode, isTrue); }); testWidgets('should_keep_home_feed_size_when_keyboard_insets_change', ( tester, ) async { addTearDown(tester.view.resetViewInsets); await _pumpApp(tester); final feed = find.byKey(const Key('home-feed-black-background')); final before = tester.getRect(feed); tester.view.viewInsets = const FakeViewPadding(bottom: 300); await tester.pump(); expect( tester .widget(find.byType(Scaffold).first) .resizeToAvoidBottomInset, isFalse, ); expect(tester.getRect(feed), before); }); testWidgets('should_open_full_player_from_full_series_bar', (tester) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-full-series'))); await tester.pumpAndSettle(); final player = tester.widget(find.byType(PlayerPage).last); expect(player.homeMode, isFalse); }); testWidgets('should_switch_to_anime_channel', (tester) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-tab-anime'))); await tester.pumpAndSettle(); expect(find.text('Coming soon'), findsNothing); expect( tester.widget(find.text('Anime')).style?.fontWeight, FontWeight.w800, ); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); }); testWidgets('should_keep_player_when_first_anime_switch_is_loading', ( tester, ) async { final catalog = _PendingAnimeCatalog(); await _pumpApp(tester, catalog: catalog); for (var attempt = 0; attempt < 5 && catalog.animeCalls == 0; attempt++) { await tester.pump(); } expect(catalog.animeCalls, 1); await tester.tap(find.byKey(const Key('home-feed-tab-anime'))); await tester.pump(); expect(find.byType(PlayerPage), findsOneWidget); expect(find.byKey(const Key('home-feed-channel-loading')), findsOneWidget); catalog.anime.complete(FeedPage(dramas: [demoDramas[1]])); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-channel-loading')), findsNothing); expect( tester.widget(find.byType(PlayerPage)).feedChannel, HomeFeedChannel.anime, ); }); testWidgets('should_show_empty_state_when_anime_has_no_dramas', ( tester, ) async { await _pumpApp(tester, catalog: _EmptyAnimeCatalog()); await tester.tap(find.byKey(const Key('home-feed-tab-anime'))); await tester.pumpAndSettle(); expect(find.byType(PlayerPage), findsNothing); expect(find.text('No stories available yet.'), findsOneWidget); }); testWidgets('should_refresh_feed_without_blank_loading', (tester) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-refresh'))); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); }); testWidgets('should_keep_feed_rail_above_bottom_panels', (tester) async { await _pumpApp(tester); final railTop = tester.getCenter(find.byKey(const Key('rail-save'))).dy; final railBottom = tester .getRect(find.byKey(const Key('rail-share'))) .bottom; final seriesTop = tester .getRect(find.byKey(const Key('home-feed-full-series'))) .top; final titleRight = tester .getRect(find.byKey(const Key('home-feed-title'))) .right; final railLeft = tester.getRect(find.byKey(const Key('rail-save'))).left; expect(railTop, greaterThan(844 * 0.45)); expect(seriesTop - railBottom, closeTo(12, 1)); expect(titleRight, lessThanOrEqualTo(railLeft + 2)); }); testWidgets('should_keep_feed_header_and_progress_after_clear', ( tester, ) async { await _pumpApp(tester); 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(); await tester.tap(find.byKey(const Key('player-more-clear'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-refresh')), findsOneWidget); expect(find.byKey(const Key('home-feed-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('home-feed-search')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); expect(find.byKey(const Key('rail-save')), findsNothing); expect(find.byKey(const Key('home-feed-title')), findsNothing); final exit = tester.getRect( find.byKey(const Key('home-feed-exit-clear-screen')), ); expect(exit.right, greaterThan(340)); expect(exit.center.dy, greaterThan(600)); await tester.tap(find.byKey(const Key('home-feed-exit-clear-screen'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('rail-save')), findsOneWidget); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); }); testWidgets('should_boost_feed_on_side_long_press', (tester) async { await _pumpApp(tester); expect(find.byKey(const Key('home-feed-title')), 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('home-feed-speed-boost-hint')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-title')), findsNothing); expect(find.byKey(const Key('rail-save')), findsNothing); expect(find.byKey(const Key('home-feed-search')), findsNothing); await gesture.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-speed-boost-hint')), findsNothing); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); expect(find.byKey(const Key('rail-save')), findsOneWidget); expect(find.byKey(const Key('home-feed-search')), findsOneWidget); }); testWidgets('should_hide_exit_clear_button_while_boosting', (tester) async { await _pumpApp(tester); final moreGesture = await tester.startGesture(const Offset(195, 300)); await tester.pump(const Duration(milliseconds: 700)); await tester.pump(); await moreGesture.up(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('player-more-clear'))); await tester.pumpAndSettle(); expect( find.byKey(const Key('home-feed-exit-clear-screen')), findsOneWidget, ); final boostGesture = await tester.startGesture(const Offset(60, 300)); await tester.pump(const Duration(milliseconds: 700)); await tester.pump(); expect(find.byKey(const Key('home-feed-speed-boost-hint')), findsOneWidget); expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); expect(find.byKey(const Key('home-feed-exit-clear-screen')), findsNothing); expect(find.byKey(const Key('home-feed-search')), findsNothing); await boostGesture.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-speed-boost-hint')), findsNothing); expect( find.byKey(const Key('home-feed-exit-clear-screen')), findsOneWidget, ); expect(find.byKey(const Key('home-feed-search')), findsOneWidget); }); testWidgets('should_resume_playback_when_boosting_while_paused', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-video-toggle'))); await tester.pump(); expect(find.byKey(const Key('home-feed-center-play')), 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('home-feed-speed-boost-hint')), findsOneWidget); expect(find.textContaining('2.0'), findsWidgets); expect(find.byKey(const Key('home-feed-center-play')), findsNothing); await gesture.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-speed-boost-hint')), findsNothing); expect(find.byKey(const Key('home-feed-center-play')), findsNothing); }); testWidgets('should_open_more_sheet_on_center_long_press', (tester) async { await _pumpApp(tester); 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); expect(find.byKey(const Key('home-feed-clear')), findsNothing); expect(find.byKey(const Key('home-feed-download')), findsNothing); expect(find.byKey(const Key('home-feed-pip')), findsNothing); }); testWidgets('should_restore_light_theme_after_leaving_feed_tab', ( tester, ) async { await _pumpApp(tester); expect(_hasLightStatusBar(tester), isTrue); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-page-gradient')), findsOneWidget); expect(_hasLightStatusBar(tester), isFalse); }); testWidgets('should_switch_feed_drama_when_swiped_up', (tester) async { await _pumpApp(tester); final before = tester .widget(find.byKey(const Key('home-feed-title'))) .data; await tester.drag(find.byType(PageView).first, const Offset(0, -600)); await tester.pumpAndSettle(); final after = tester .widget(find.byKey(const Key('home-feed-title'))) .data; expect(after, isNot(before)); }); testWidgets('should_keep_feed_header_fixed_when_swiping', (tester) async { await _pumpApp(tester); final before = tester.getRect(find.byKey(const Key('home-feed-search'))); await tester.drag(find.byType(PageView).first, const Offset(0, -600)); await tester.pumpAndSettle(); final after = tester.getRect(find.byKey(const Key('home-feed-search'))); expect(after.top, closeTo(before.top, 0.5)); expect(find.byKey(const Key('home-feed-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('home-feed-refresh')), findsOneWidget); }); testWidgets('should_resume_feed_after_returning_from_full_player', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-video-toggle'))); await tester.pump(); 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( tester.widget(find.byType(PlayerPage).last).homeMode, isFalse, ); await tester.tap(find.byKey(const Key('player-back'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); expect(find.byKey(const Key('home-feed-center-play')), findsNothing); }); testWidgets('should_hide_feed_when_switching_to_theater', (tester) async { await _pumpApp(tester); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-title')), findsNothing); expect(find.byKey(const Key('discover-search')), findsOneWidget); }); testWidgets('should_open_theater_search_when_feed_search_is_tapped', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('home-feed-search'))); await tester.pumpAndSettle(); final field = tester.widget( find.byKey(const Key('discover-search')), ); expect(field.focusNode?.hasFocus, isTrue); }); testWidgets('should_not_render_theater_language_filters', (tester) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('theater-language-chips')), findsNothing); expect(find.byKey(const Key('theater-category-chips')), findsOneWidget); expect(find.byKey(const Key('discover-search')), findsOneWidget); expect(find.byKey(const Key('discover-refresh')), findsOneWidget); expect( find.byKey(const Key('home-drama-card-the-signal-demo')), findsOneWidget, ); }); testWidgets('should_open_fullscreen_player_when_theater_card_is_tapped', ( tester, ) async { await _pumpApp(tester); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); final card = find.byKey(const Key('home-drama-card-the-signal-demo')); 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(); final player = tester.widget(find.byType(PlayerPage).last); expect(player.homeMode, isFalse); }); testWidgets('should_fit_home_feed_and_theater_on_320_width', (tester) async { await _pumpApp(tester, size: const Size(320, 568)); expect(find.byKey(const Key('home-feed-title')), findsOneWidget); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('tab-theater'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('theater-category-chips')), findsOneWidget); expect(find.byKey(const Key('theater-language-chips')), findsNothing); expect(tester.takeException(), isNull); }); } const _tabKeys = [ Key('tab-home'), Key('tab-theater'), Key('tab-create'), Key('tab-mine'), ]; bool _hasLightStatusBar(WidgetTester tester) => tester .widgetList>( find.byType(AnnotatedRegion), ) .any((region) => region.value.statusBarIconBrightness == Brightness.light); Future _pumpApp( WidgetTester tester, { Size size = const Size(390, 844), FakeCatalogRepository? catalog, }) 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: createWidgetPlayer, catalogRepository: repository, categoryRepository: FakeCategoryRepository(catalog: repository), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: repository.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), haptics: FakeHaptics(), ), ); await tester.pumpAndSettle(); } class _PendingAnimeCatalog extends FakeCatalogRepository { final anime = Completer(); int animeCalls = 0; @override Future fetchFeed({ List types = const [], String? categoryId, String? cursor, int size = 10, }) { if (types.contains('ANIME')) { animeCalls++; return anime.future; } return super.fetchFeed( types: types, categoryId: categoryId, cursor: cursor, size: size, ); } } class _EmptyAnimeCatalog extends FakeCatalogRepository { @override Future fetchFeed({ List types = const [], String? categoryId, String? cursor, int size = 10, }) { if (types.contains('ANIME')) { return Future.value(const FeedPage(dramas: [])); } return super.fetchFeed( types: types, categoryId: categoryId, cursor: cursor, size: size, ); } }