// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.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/core/data/models/reward.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.dart'; import 'package:fruitshorts/core/data/repositories/reward_repository.dart'; import 'package:fruitshorts/core/data/services/auth_controller.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart'; import 'package:fruitshorts/core/data/services/reward_controller.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/reward/reward_page.dart'; void main() { testWidgets('should_render_tasks_and_switch_to_activities_when_signed_in', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final auth = _TestAuthController(signedIn: true); final rewards = RewardController(_RewardPageRepository()); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider.value(value: rewards), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: RewardPage(active: true)), ), ), ); await tester.pump(const Duration(milliseconds: 2)); await tester.pumpAndSettle(); expect(find.text('Reward Center'), findsOneWidget); expect(find.text('My coins'), findsOneWidget); expect(find.text('Daily task'), findsOneWidget); expect(find.text('Check in +10 coins'), findsOneWidget); expect(find.byKey(const Key('reward-back-button')), findsOneWidget); expect(find.byKey(const Key('reward-help-button')), findsOneWidget); expect(find.byKey(const Key('reward-balance')), findsOneWidget); expect(find.byKey(const Key('reward-balance-coin-icon')), findsOneWidget); expect(find.byKey(const Key('reward-checkin-card')), findsOneWidget); expect(find.byKey(const Key('reward-checkin-action')), findsOneWidget); expect(find.byKey(const Key('reward-section-DAILY')), findsOneWidget); expect(find.byKey(const Key('reward-task-card-DAILY')), findsOneWidget); await tester.tap(find.byKey(const Key('reward-tab-activities'))); await tester.pumpAndSettle(); expect(find.text('Spring event'), findsOneWidget); expect(find.text('Daily task'), findsNothing); await tester.tap(find.byKey(const Key('reward-tab-tasks'))); await tester.pumpAndSettle(); expect(find.text('Daily task'), findsOneWidget); expect(find.text('Spring event'), findsNothing); expect( tester.getSize(find.byKey(const Key('reward-tab-indicator-tasks'))).width, 24, ); auth.dispose(); rewards.dispose(); }); testWidgets('should_show_reward_content_and_gate_check_in_when_signed_out', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final auth = _TestAuthController(signedIn: false); final repository = _RewardPageRepository(); final rewards = RewardController(repository); await _pumpRewardPage(tester, auth: auth, rewards: rewards); expect(find.byKey(const Key('auth-gate-panel')), findsNothing); expect(find.text('Daily task'), findsOneWidget); expect(find.text('Check in +10 coins'), findsOneWidget); expect(repository.overviewCalls, 1); await tester.tap(find.text('Check in +10 coins')); await tester.pumpAndSettle(); expect(find.text('Sign in to continue'), findsOneWidget); expect(repository.checkInCalls, 0); auth.dispose(); rewards.dispose(); }); testWidgets('should_use_reward_dialog_style_when_help_is_opened', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final auth = _TestAuthController(signedIn: false); final rewards = RewardController(_RewardPageRepository()); await _pumpRewardPage(tester, auth: auth, rewards: rewards); await tester.tap(find.byKey(const Key('reward-help-button'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('reward-help-dialog')), findsOneWidget); expect(find.text('About rewards'), findsOneWidget); expect( find.text( 'Complete check-ins and tasks to earn reward coins. Rewards and task rules are shown on this page.', ), findsOneWidget, ); expect(find.byKey(const Key('reward-help-close')), findsOneWidget); expect(find.byType(AlertDialog), findsNothing); await tester.tap(find.byKey(const Key('reward-help-close'))); await tester.pumpAndSettle(); expect(find.byKey(const Key('reward-help-dialog')), findsNothing); auth.dispose(); rewards.dispose(); }); testWidgets( 'should_show_retry_without_reloading_forever_when_overview_fails', (tester) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final auth = _TestAuthController(signedIn: false); final repository = _FailingRewardPageRepository(); final rewards = RewardController(repository); await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider.value(value: rewards), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: RewardPage(active: true)), ), ), ); await tester.pump(const Duration(milliseconds: 2)); await tester.pump(const Duration(milliseconds: 2)); expect(repository.overviewCalls, 1); expect(find.byType(CircularProgressIndicator), findsNothing); expect(find.byType(FilledButton), findsOneWidget); await tester.tap(find.byType(FilledButton)); await tester.pump(const Duration(milliseconds: 2)); expect(repository.overviewCalls, 2); expect(find.byType(CircularProgressIndicator), findsNothing); auth.dispose(); rewards.dispose(); }, ); testWidgets('should_reload_overview_when_auth_state_changes', (tester) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); final auth = _TestAuthController(signedIn: false); final repository = _RewardPageRepository(); final rewards = RewardController(repository); await _pumpRewardPage(tester, auth: auth, rewards: rewards); expect(repository.overviewCalls, 1); auth.updateSignedIn(true); await tester.pump(const Duration(milliseconds: 2)); await tester.pumpAndSettle(); expect(repository.overviewCalls, 2); auth.dispose(); rewards.dispose(); }); testWidgets('should_place_header_actions_at_edges_when_width_is_compact', ( tester, ) async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); await tester.binding.setSurfaceSize(const Size(393, 852)); addTearDown(() => tester.binding.setSurfaceSize(null)); final auth = _TestAuthController(signedIn: false); final rewards = RewardController(_RewardPageRepository()); await _pumpRewardPage(tester, auth: auth, rewards: rewards); final back = tester.getCenter(find.byKey(const Key('reward-back-button'))); final help = tester.getCenter(find.byKey(const Key('reward-help-button'))); expect(back.dx, lessThan(40)); expect(help.dx, greaterThan(350)); expect(find.text('Hidden check-in task'), findsNothing); expect(tester.takeException(), isNull); auth.dispose(); rewards.dispose(); }); } Future _pumpRewardPage( WidgetTester tester, { required AuthController auth, required RewardController rewards, }) async { await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider.value(value: rewards), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const Scaffold(body: RewardPage(active: true)), ), ), ); await tester.pump(const Duration(milliseconds: 2)); await tester.pumpAndSettle(); } class _TestAuthController extends AuthController { _TestAuthController({required this.signedIn}) : super( AuthApi(ApiClient(baseUrl: 'http://127.0.0.1:8081')), AuthSessionStore(), ); bool signedIn; @override bool get isSignedIn => signedIn; void updateSignedIn(bool value) { signedIn = value; notifyListeners(); } } class _RewardPageRepository implements RewardRepository { int overviewCalls = 0; int checkInCalls = 0; @override Future fetchOverview(String locale) async { overviewCalls += 1; return const RewardOverview( serverDate: '2026-11-05', timezone: 'Asia/Jakarta', coinBalance: 120, checkin: RewardCheckinState( taskId: 'checkin', checkedInToday: false, streak: 2, currentDay: 3, days: [ RewardCheckinDay( dayIndex: 1, rewardCoin: 10, checked: false, current: true, ), RewardCheckinDay( dayIndex: 2, rewardCoin: 10, checked: false, current: false, ), RewardCheckinDay( dayIndex: 3, rewardCoin: 10, checked: false, current: false, ), RewardCheckinDay( dayIndex: 4, rewardCoin: 10, checked: false, current: false, ), RewardCheckinDay( dayIndex: 5, rewardCoin: 10, checked: false, current: false, ), RewardCheckinDay( dayIndex: 6, rewardCoin: 10, checked: false, current: false, ), RewardCheckinDay( dayIndex: 7, rewardCoin: 10, checked: false, current: false, ), ], ), sections: [ RewardSection( code: 'CHECK_IN', title: 'Check-in section', tasks: [ RewardTask( id: 'checkin-task', code: 'CHECK_IN', type: 'DAILY_CHECK_IN', title: 'Hidden check-in task', subtitle: '', ctaText: 'Check in', actionType: 'NONE', actionUrl: '', progressValue: 0, targetValue: 1, rewardCoin: 10, status: 'IN_PROGRESS', ), ], ), 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: '', actionType: 'OPEN_HOME', actionUrl: '', progressValue: 2, targetValue: 5, rewardCoin: 20, status: 'IN_PROGRESS', ), ], ), ], activities: [ RewardActivity( id: 'spring', code: 'SPRING', title: 'Spring event', summary: 'Earn extra coins this week', ctaText: '', actionType: '', actionUrl: '', ), ], ); } @override Future checkIn() async { checkInCalls += 1; return const RewardActionResult( rewardCoin: 10, coinBalance: 130, alreadyCompleted: false, ); } @override Future claim(String taskId) async { return const RewardActionResult( rewardCoin: 20, coinBalance: 140, alreadyCompleted: false, ); } } class _FailingRewardPageRepository implements RewardRepository { int overviewCalls = 0; @override Future fetchOverview(String locale) async { overviewCalls += 1; throw ApiException(500, 'offline'); } @override Future checkIn() async { throw ApiException(401, 'login'); } @override Future claim(String taskId) async { throw ApiException(401, 'login'); } }