// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/core/data/models/reward.dart'; import 'package:fruitshorts/core/data/models/wallet.dart'; import 'package:fruitshorts/core/data/repositories/commerce_repository.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/reward_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; import 'package:fruitshorts/core/widgets/root_nav_glyph.dart'; import 'package:fruitshorts/pages/following/following_page.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'; const _keys = [ Key('home-tabs-bar'), Key('tab-home'), Key('tab-recommend'), Key('tab-reward'), Key('tab-follow'), Key('tab-mine'), ]; void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({ 'auth.accessToken': 'test-access', 'auth.refreshToken': 'test-refresh', 'auth.user': '{"uid":"9","nickname":"Viewer","avatarUrl":""}', }); }); testWidgets('should_theme_five_tabs_and_switch_selected_state_when_tapped', ( tester, ) async { final haptics = FakeHaptics(); await _pumpApp(tester, haptics: haptics); haptics.reset(); for (final key in _keys) { expect(find.byKey(key), findsOneWidget); } // 首页顶部改为悬浮内层导航:搜索 + 礼物(钱包)入口。 expect(find.byKey(const Key('recommend-top-nav')), findsOneWidget); final searchEntry = find.byKey(const Key('recommend-search')); final walletEntry = find.byKey(const Key('recommend-gift')); expect( tester.getSize(searchEntry), const Size.square(AppDimens.homeActionSlot), ); expect( tester.getSize(walletEntry), const Size.square(AppDimens.homeActionSlot), ); expect(tester.widget(searchEntry).iconSize, 26); expect(tester.widget(walletEntry).iconSize, 26); expect( tester.getCenter(searchEntry).dy, closeTo(tester.getCenter(walletEntry).dy, 0.1), ); expect(tester.getTopRight(walletEntry).dx, closeTo(390 - 8, 0.1)); expect( tester.getTopRight(searchEntry).dx, closeTo(tester.getTopLeft(walletEntry).dx, 0.1), ); expect(find.byKey(const Key('mine-mini-player')), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); for (final key in _keys.skip(1)) { expect(tester.getSize(find.byKey(key)).height, greaterThanOrEqualTo(48)); } final tabCenters = _keys .skip(1) .map((key) => tester.getCenter(find.byKey(key)).dx) .toList(); expect(tabCenters, orderedEquals(tabCenters.toList()..sort())); expect( find.descendant( of: find.byKey(const Key('tab-reward')), matching: find.text('Rewards'), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.text('Home'), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-reward')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-recommend')), matching: find.text('Recommend'), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-follow')), matching: find.text('Following'), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-mine')), matching: find.text('My'), ), findsOneWidget, ); final barFinder = find.byKey(const Key('home-tabs-bar')); final bar = tester.widget(barFinder); expect(bar.color, AppColors.navBackground); expect(bar.decoration, isNull); expect( tester .widget( find.descendant( of: find.byKey(const Key('tab-recommend')), matching: find.text('Recommend'), ), ) .style ?.color, AppColors.navUnselected, ); final selectedHomeIcon = tester.widget( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.byType(RootNavGlyph), ), ); expect(selectedHomeIcon.kind, RootNavGlyphKind.home); expect(selectedHomeIcon.selected, isTrue); expect(selectedHomeIcon.size, AppDimens.navIcon); expect( tester.getSize( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.byType(RootNavGlyph), ), ), const Size.square(AppDimens.navIcon), ); expect( find.descendant( of: find.byKey(const Key('tab-recommend')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-follow')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); expect( find.descendant( of: find.byKey(const Key('tab-mine')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); expect(haptics.selectionClicks, 1); final theaterBar = tester.widget(barFinder); expect(theaterBar.color, AppColors.navBackground); expect(theaterBar.decoration, isNull); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); expect(haptics.selectionClicks, 1); expect( find.descendant( of: find.byKey(const Key('tab-recommend')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); expect( tester .widget( find.descendant( of: find.byKey(const Key('tab-recommend')), matching: find.text('Recommend'), ), ) .style ?.color, AppColors.navSelected, ); expect( tester .widget( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.text('Home'), ), ) .style ?.color, AppColors.navUnselected, ); expect( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); expect( tester .widget( find.descendant( of: find.byKey(const Key('tab-home')), matching: find.byType(RootNavGlyph), ), ) .selected, isFalse, ); }); testWidgets( 'should_map_five_tabs_to_home_recommend_reward_following_and_me', (tester) async { // 追剧页头部的双语分段文案在测试字体(Ahem)下比真机字体宽约 98pt, // 这里放宽视口宽度,避免测字体撑破 390 宽度下的分段行。 await _pumpApp(tester, size: const Size(520, 844)); expect(find.byKey(const Key('tab-home')), findsOneWidget); expect(find.byKey(const Key('tab-recommend')), findsOneWidget); expect(find.byKey(const Key('tab-reward')), findsOneWidget); expect(find.byKey(const Key('tab-follow')), findsOneWidget); expect(find.byKey(const Key('tab-mine')), findsOneWidget); expect(find.byKey(const Key('tab-create')), findsNothing); expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); await tester.tap(find.byKey(const Key('tab-recommend'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-channel-nav')), findsOneWidget); _expectNoFloatingOverlays(); await tester.tap(find.byKey(const Key('tab-reward'))); await tester.pumpAndSettle(); expect(find.text('Reward Center'), findsOneWidget); expect(find.text('Daily task'), findsOneWidget); _expectNoFloatingOverlays(); await tester.tap(find.byKey(const Key('tab-follow'))); await tester.pumpAndSettle(); final favorites = find.byType(FollowingPage); expect(favorites, findsOneWidget); expect(find.byKey(const Key('following-page')), findsOneWidget); expect(find.byType(AppBottomNav), findsOneWidget); expect( find.descendant(of: favorites, matching: find.byType(Scaffold)), findsNothing, ); expect(Navigator.canPop(tester.element(favorites)), isFalse); _expectNoFloatingOverlays(); expect( find.descendant( of: find.byKey(const Key('tab-follow')), matching: find.byType(RootNavGlyph), ), findsOneWidget, ); await tester.tap(find.byKey(const Key('tab-mine'))); await tester.pumpAndSettle(); expect(find.text('Viewer'), findsOneWidget); expect(find.byKey(const Key('mine-menu')), findsOneWidget); expect(find.byKey(const Key('mine-wallet')), findsOneWidget); expect(find.byKey(const Key('mine-edit')), findsOneWidget); expect(find.byKey(const Key('mine-library-tab-0')), findsNothing); _expectNoFloatingOverlays(); await tester.tap(find.byKey(const Key('tab-follow'))); await tester.pumpAndSettle(); expect(favorites, findsOneWidget); expect(find.byType(AppBottomNav), findsOneWidget); expect(Navigator.canPop(tester.element(favorites)), isFalse); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget); }, ); testWidgets('should_not_haptic_when_ordinary_list_item_is_opened', ( tester, ) async { final haptics = FakeHaptics(); await _pumpApp(tester, haptics: haptics); await tester.tap(find.byKey(const Key('tab-mine'))); await tester.pumpAndSettle(); haptics.reset(); await tester.ensureVisible(find.byKey(const Key('mine-settings'))); await tester.tap(find.byKey(const Key('mine-settings'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('settings-page')), findsOneWidget); expect(haptics.calls, isEmpty); }); } Future _pumpApp( WidgetTester tester, { FakeHaptics? haptics, Size size = const Size(390, 844), }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final catalog = FakeCatalogRepository(); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), commerceRepository: const _WalletCommerceRepository(), rewardRepository: const _RewardRepository(), haptics: haptics ?? FakeHaptics(), ), ); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('tab-home'))); await tester.pumpAndSettle(); } class _WalletCommerceRepository implements CommerceRepository { const _WalletCommerceRepository(); @override Future fetchWallet() async { return const WalletSnapshot( wallet: CoinWallet( coinBalance: 320, totalRechargedCoin: 500, totalConsumedCoin: 180, totalRefundedCoin: 0, ), ); } @override Future unlock(String episodeId) async { return const UnlockResult( unlocked: true, accessType: 'UNLOCKED', coinCost: 0, coinBalance: 320, ); } } class _RewardRepository implements RewardRepository { const _RewardRepository(); @override Future fetchOverview(String locale) async { return const RewardOverview( serverDate: '2026-11-05', timezone: 'Asia/Jakarta', coinBalance: 120, checkin: RewardCheckinState( taskId: 'checkin', checkedInToday: false, streak: 1, currentDay: 2, ), sections: [ RewardSection( code: 'DAILY', title: 'Daily task', tasks: [ RewardTask( id: 'watch', code: 'WATCH', type: 'WATCH_DURATION', title: 'Watch a drama', subtitle: 'Watch for five minutes', ctaText: 'Go', actionType: 'OPEN_HOME', actionUrl: '', progressValue: 0, targetValue: 300, rewardCoin: 20, status: 'IN_PROGRESS', ), ], ), ], ); } @override Future checkIn() async { return const RewardActionResult( rewardCoin: 10, coinBalance: 130, alreadyCompleted: false, ); } @override Future claim(String taskId) async { return const RewardActionResult( rewardCoin: 20, coinBalance: 140, alreadyCompleted: false, ); } } void _expectNoFloatingOverlays() { expect(find.byKey(const Key('mine-mini-player')), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); }