415 lines
13 KiB
Dart
415 lines
13 KiB
Dart
// 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<AuthController>.value(value: auth),
|
|
ChangeNotifierProvider<RewardController>.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<AuthController>.value(value: auth),
|
|
ChangeNotifierProvider<RewardController>.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<void> _pumpRewardPage(
|
|
WidgetTester tester, {
|
|
required AuthController auth,
|
|
required RewardController rewards,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
|
ChangeNotifierProvider<RewardController>.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<RewardOverview> 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<RewardActionResult> checkIn() async {
|
|
checkInCalls += 1;
|
|
return const RewardActionResult(
|
|
rewardCoin: 10,
|
|
coinBalance: 130,
|
|
alreadyCompleted: false,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<RewardActionResult> claim(String taskId) async {
|
|
return const RewardActionResult(
|
|
rewardCoin: 20,
|
|
coinBalance: 140,
|
|
alreadyCompleted: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FailingRewardPageRepository implements RewardRepository {
|
|
int overviewCalls = 0;
|
|
|
|
@override
|
|
Future<RewardOverview> fetchOverview(String locale) async {
|
|
overviewCalls += 1;
|
|
throw ApiException(500, 'offline');
|
|
}
|
|
|
|
@override
|
|
Future<RewardActionResult> checkIn() async {
|
|
throw ApiException(401, 'login');
|
|
}
|
|
|
|
@override
|
|
Future<RewardActionResult> claim(String taskId) async {
|
|
throw ApiException(401, 'login');
|
|
}
|
|
}
|