// 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/wallet.dart'; import 'package:fruitshorts/core/data/models/watch_progress.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/watch_history_repository.dart'; import 'package:fruitshorts/core/theme/app_colors.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/theme/app_theme.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/auth/auth_view.dart'; import 'package:fruitshorts/pages/mine/about_page.dart'; import 'package:fruitshorts/pages/mine/mine_types.dart'; import 'package:fruitshorts/pages/mine/widgets/mine_menu.dart'; import 'package:fruitshorts/pages/store/vip_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_interaction_repositories.dart'; import '../support/fake_notice_repository.dart'; import '../support/widget_player.dart'; void main() { testWidgets('should_render_visitor_user_vip_and_seven_grid_entries', ( tester, ) async { await _pumpApp(tester, signedIn: false); await _openMine(tester); expect(find.byKey(const Key('mine-user-row')), findsOneWidget); expect(find.byKey(const Key('mine-avatar')), findsOneWidget); expect(find.text('Visitor'), findsOneWidget); expect(find.text('UID: --'), findsOneWidget); expect(find.byKey(const Key('mine-uid-copy')), findsOneWidget); final uidRowSize = tester.getSize(find.byKey(const Key('mine-uid'))); expect(uidRowSize.height, closeTo(AppDimens.mineUidRowHeight, 0.1)); final nicknameRect = tester.getRect(find.byKey(const Key('mine-nickname'))); final uidRect = tester.getRect(find.byKey(const Key('mine-uid'))); expect(uidRect.top - nicknameRect.bottom, lessThanOrEqualTo(6)); final nickname = tester.widget( find.byKey(const Key('mine-nickname')), ); expect(nickname.style?.fontSize, AppDimens.mineUserName); expect(nickname.style?.fontWeight, FontWeight.w700); expect(nickname.style?.height, AppDimens.mineUserNameLineHeight); final uid = tester.widget(find.text('UID: --')); expect(uid.style?.fontSize, AppDimens.mineUid); expect(uid.style?.color, AppColors.textTertiary); final copyIcon = tester.widget( find.descendant( of: find.byKey(const Key('mine-uid-copy')), matching: find.byType(Icon), ), ); expect(copyIcon.size, AppDimens.mineCopyIcon); expect(copyIcon.color, AppColors.textTertiary); expect(find.byKey(const Key('mine-login')), findsOneWidget); expect(find.byKey(const Key('mine-edit')), findsNothing); expect(find.byKey(const Key('mine-vip')), findsOneWidget); expect(find.byKey(const Key('mine-vip-open')), findsOneWidget); expect(find.byKey(const Key('mine-continue-watching')), findsNothing); expect(find.byKey(const Key('mine-stats')), findsOneWidget); expect(find.text('—'), findsNWidgets(3)); expect(find.byKey(const Key('mine-menu')), findsOneWidget); expect(find.byKey(const Key('mine-more-services')), findsOneWidget); expect(find.byKey(const Key('mine-mini-player')), findsNothing); final surface = tester.widget( find.byKey(const Key('mine-page-surface')), ); final surfaceDecoration = surface.decoration as BoxDecoration; final background = surfaceDecoration.gradient! as LinearGradient; expect(background.begin, Alignment.topCenter); expect(background.end, Alignment.bottomCenter); expect(background.colors, const [ AppColors.mineGradientTop, AppColors.mineGradientMiddle, AppColors.mineGradientBottom, ]); expect(background.stops, const [0, 0.4, 0.75]); expect( find.descendant( of: find.byKey(const Key('mine-page-surface')), matching: find.text('My'), ), findsNothing, ); final glow = tester.widget( find.byKey(const Key('mine-page-glow')), ); final glowDecoration = glow.decoration as BoxDecoration; final radialGlow = glowDecoration.gradient! as RadialGradient; expect(radialGlow.center, const Alignment(0, -0.95)); expect(radialGlow.radius, 0.9); expect(radialGlow.colors, const [ AppColors.mineGradientGlow, AppColors.transparent, ]); for (final oldShortcut in const [ Key('mine-watchlist'), Key('mine-history-shortcut'), Key('mine-downloads-shortcut'), ]) { expect(find.byKey(oldShortcut), findsNothing); } const entryKeys = [ Key('mine-wallet'), Key('mine-downloads'), Key('mine-history'), Key('mine-watch-preferences'), Key('mine-help'), Key('mine-language'), Key('mine-settings'), ]; expect(entryKeys, hasLength(7)); for (final entryKey in entryKeys) { expect(find.byKey(entryKey), findsOneWidget); } const coloredIcons = [ ( Key('mine-wallet'), Icons.account_balance_wallet_outlined, AppColors.mineIconWallet, ), ( Key('mine-downloads'), Icons.download_outlined, AppColors.mineIconDownloads, ), (Key('mine-history'), Icons.history_rounded, AppColors.mineIconHistory), ( Key('mine-watch-preferences'), Icons.tune_rounded, AppColors.mineIconPreferences, ), ( Key('mine-help'), Icons.support_agent_rounded, AppColors.mineIconSupport, ), ( Key('mine-language'), Icons.language_rounded, AppColors.mineIconLanguage, ), ( Key('mine-settings'), Icons.settings_outlined, AppColors.mineIconSettings, ), ]; for (final entry in coloredIcons) { final icon = tester.widget( find.descendant( of: find.byKey(entry.$1), matching: find.byIcon(entry.$2), ), ); expect(icon.color, entry.$3); expect(icon.size, AppDimens.mineGridIcon); } for (var index = 0; index < 8; index++) { expect(find.byKey(Key('mine-menu-cell-$index')), findsOneWidget); } expect( find.descendant( of: find.byKey(const Key('mine-menu-cell-7')), matching: find.byType(Icon), ), findsNothing, ); expect( find.descendant( of: find.byKey(const Key('mine-menu')), matching: find.byType(ListTile), ), findsNothing, ); final positions = [ for (final entryKey in entryKeys) tester.getTopLeft(find.byKey(entryKey)), ]; expect( positions.take(4).map((position) => position.dy).toSet(), hasLength(1), ); expect( positions.skip(4).map((position) => position.dy).toSet(), hasLength(1), ); expect(positions[1].dx, greaterThan(positions[0].dx)); expect(positions[2].dx, greaterThan(positions[1].dx)); expect(positions[3].dx, greaterThan(positions[2].dx)); expect(positions[5].dx, greaterThan(positions[4].dx)); expect(positions[6].dx, greaterThan(positions[5].dx)); expect(tester.getTopLeft(find.byKey(const Key('mine-menu'))).dx, 14); final menu = tester.widget(find.byKey(const Key('mine-menu'))); final menuDecoration = menu.decoration! as BoxDecoration; expect(menuDecoration.color, AppColors.mineGridSurface); expect( menuDecoration.borderRadius, BorderRadius.circular(AppDimens.mineGridRadius), ); expect(menuDecoration.border, isNull); expect(menuDecoration.boxShadow, isNull); final table = tester.widget(find.byKey(const Key('mine-menu-grid'))); final border = table.border!; expect(border.horizontalInside.color, AppColors.mineGridDivider); expect(border.verticalInside.color, AppColors.mineGridDivider); final vip = tester.widget(find.byKey(const Key('mine-vip'))); final vipDecoration = vip.decoration! as BoxDecoration; expect((vipDecoration.gradient! as LinearGradient).colors, const [ AppColors.vipGoldStart, AppColors.vipGoldEnd, ]); expect(vipDecoration.boxShadow, isNull); final order = [ tester.getTopLeft(find.byKey(const Key('mine-user-row'))).dy, tester.getTopLeft(find.byKey(const Key('mine-vip'))).dy, tester.getTopLeft(find.byKey(const Key('mine-stats'))).dy, tester.getTopLeft(find.byKey(const Key('mine-menu'))).dy, tester.getTopLeft(find.byKey(const Key('mine-more-services'))).dy, ]; expect(order, [...order]..sort()); expect(tester.takeException(), isNull); await tester.tap(find.byKey(const Key('mine-login'))); await tester.pumpAndSettle(); expect(find.byType(AuthPage), findsOneWidget); }); testWidgets('should_render_signed_in_user_without_floating_mini_player', ( tester, ) async { await _pumpApp( tester, signedIn: true, history: InMemoryWatchHistoryRepository(seed: [_recent]), ); await _openMine(tester); expect(find.text('Viewer'), findsOneWidget); expect(find.text('UID: U-100'), findsOneWidget); expect(find.byKey(const Key('mine-edit')), findsOneWidget); expect(find.byKey(const Key('mine-login')), findsNothing); expect(find.byKey(const Key('mine-mini-player')), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); expect(find.byKey(const Key('mine-continue-watching')), findsOneWidget); expect(find.byKey(const Key('mine-continue-all')), findsOneWidget); expect(find.text('Recent drama'), findsOneWidget); expect(find.text('Episode 3'), findsWidgets); String statValue(Key key) { return tester .widget( find .descendant(of: find.byKey(key), matching: find.byType(Text)) .first, ) .data!; } expect(statValue(const Key('mine-stat-following')), '3'); expect(statValue(const Key('mine-stat-watch-records')), '1'); expect(statValue(const Key('mine-stat-coins')), '320'); final signedInOrder = [ tester.getTopLeft(find.byKey(const Key('mine-user-row'))).dy, tester.getTopLeft(find.byKey(const Key('mine-vip'))).dy, tester.getTopLeft(find.byKey(const Key('mine-continue-watching'))).dy, tester.getTopLeft(find.byKey(const Key('mine-stats'))).dy, tester.getTopLeft(find.byKey(const Key('mine-menu'))).dy, tester.getTopLeft(find.byKey(const Key('mine-more-services'))).dy, ]; expect(signedInOrder, [...signedInOrder]..sort()); await tester.tap(find.byKey(const Key('mine-edit'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-edit-profile')), findsOneWidget); expect(find.byKey(const Key('mine-edit-works')), findsOneWidget); expect(find.byKey(const Key('mine-edit-creator')), findsOneWidget); expect(find.byKey(const Key('mine-edit-notices')), findsOneWidget); await tester.tap(find.byKey(const Key('mine-edit-works'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-placeholder-page')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('should_open_more_services_and_about_page', (tester) async { await _pumpApp(tester, signedIn: false); await _openMine(tester); await tester.tap(find.byKey(const Key('mine-common-questions'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-placeholder-page')), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('mine-about'))); await tester.pumpAndSettle(); expect(find.byType(MineAboutPage), findsOneWidget); expect(find.byKey(const Key('mine-about-page')), findsOneWidget); expect(find.text('Version 1.0.0'), findsOneWidget); expect( find.text( AppLocalizations.of( tester.element(find.byType(MineAboutPage)), )!.aboutDescription, ), findsOneWidget, ); }); testWidgets('should_open_history_tab_from_continue_watching_all', ( tester, ) async { await _pumpApp( tester, signedIn: true, history: InMemoryWatchHistoryRepository(seed: [_recent]), ); await _openMine(tester); await tester.tap(find.byKey(const Key('mine-continue-all'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('following-page')), findsOneWidget); expect(find.byKey(const Key('following-history-grid')), findsOneWidget); }); testWidgets('should_open_vip_from_mine_without_floating_player', ( tester, ) async { await _pumpApp( tester, signedIn: true, history: InMemoryWatchHistoryRepository(seed: [_recent]), ); await _openMine(tester); await tester.tap(find.byKey(const Key('mine-vip-open'))); await tester.pumpAndSettle(); expect(find.byType(VipPage), findsOneWidget); await tester.binding.handlePopRoute(); await tester.pumpAndSettle(); expect(find.byKey(const Key('mine-mini-player')), findsNothing); expect(find.byType(FloatingActionButton), findsNothing); }); testWidgets('should_expose_seven_menu_destinations_in_order', (tester) async { tester.view.physicalSize = const Size(390, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); final opened = []; var languageOpened = 0; await tester.pumpWidget( MaterialApp( theme: AppTheme.dark(), locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Scaffold( body: SingleChildScrollView( padding: const EdgeInsets.all(AppDimens.pageHorizontal), child: MineMenuList( onOpen: opened.add, onLanguage: () => languageOpened++, ), ), ), ), ); const rows = [ (Key('mine-wallet'), MineDestination.wallet), (Key('mine-downloads'), MineDestination.downloads), (Key('mine-history'), MineDestination.history), (Key('mine-watch-preferences'), MineDestination.preferences), (Key('mine-help'), MineDestination.help), ]; for (final row in rows) { await tester.tap(find.byKey(row.$1)); await tester.pump(); expect(opened.last, row.$2); } await tester.tap(find.byKey(const Key('mine-language'))); await tester.pump(); expect(languageOpened, 1); await tester.tap(find.byKey(const Key('mine-settings'))); await tester.pump(); expect(opened.last, MineDestination.settings); }); } const _recent = WatchProgress( dramaId: 'city-lights-demo', episodeId: 'city-lights-demo-3', dramaTitle: 'Recent drama', dramaCover: '/files/recent.jpg', episodeNumber: 3, totalEpisodes: 8, position: Duration(seconds: 30), duration: Duration(minutes: 2), ); Future _openMine(WidgetTester tester) async { await tester.tap(find.byKey(const Key('tab-mine'))); await tester.pumpAndSettle(); } Future _pumpApp( WidgetTester tester, { required bool signedIn, WatchHistoryRepository? history, CommerceRepository? commerce, }) async { tester.view.physicalSize = const Size(390, 1000); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); SharedPreferencesAsyncPlatform.instance = signedIn ? InMemorySharedPreferencesAsync.withData({ 'auth.accessToken': 'access', 'auth.refreshToken': 'refresh', 'auth.user': '{"uid":"U-100","nickname":"Viewer","avatarUrl":""}', }) : InMemorySharedPreferencesAsync.empty(); final catalog = FakeCatalogRepository(); await tester.pumpWidget( LumaDramaApp( playerFactory: createWidgetPlayer, catalogRepository: catalog, categoryRepository: FakeCategoryRepository(catalog: catalog), watchHistoryRepository: history ?? InMemoryWatchHistoryRepository(), favoriteRepository: InMemoryFavoriteRepository(cards: catalog.dramas), playbackRepository: FakePlaybackRepository(), commentRepository: FakeCommentRepository(), interactionRepository: FakeInteractionRepository(), noticeRepository: FakeNoticeRepository(), commerceRepository: commerce ?? const _MineWalletCommerceRepository(), ), ); await tester.pumpAndSettle(); } class _MineWalletCommerceRepository implements CommerceRepository { const _MineWalletCommerceRepository(); @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, ); } }