feat(reward): 新增客户端奖励中心
This commit is contained in:
1 parent
67670d46b1
commit
bea08110c9
68 files changed
+3987
-332
No files matched your search
@@ -0,0 +1,43 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/widgets/app_press_scale.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_scale_down_on_pointer_down_and_settle_on_release', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: AppPressScale(
|
||||
key: Key('press-scale'),
|
||||
child: SizedBox.square(key: Key('press-target'), dimension: 80),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final transformFinder = find.descendant(
|
||||
of: find.byKey(const Key('press-scale')),
|
||||
matching: find.byType(Transform),
|
||||
);
|
||||
expect(tester.widget<Transform>(transformFinder).transform.entry(0, 0), 1);
|
||||
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.byKey(const Key('press-target'))),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 90));
|
||||
expect(
|
||||
tester.widget<Transform>(transformFinder).transform.entry(0, 0),
|
||||
lessThan(1),
|
||||
);
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<Transform>(transformFinder).transform.entry(0, 0), 1);
|
||||
});
|
||||
}
|
||||
@@ -114,7 +114,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
coverClip.borderRadius,
|
||||
BorderRadius.circular(AppDimens.homeCardRadius),
|
||||
BorderRadius.circular(AppDimens.coverRadius),
|
||||
);
|
||||
final titleFinder = find.descendant(
|
||||
of: card,
|
||||
|
||||
@@ -63,7 +63,7 @@ void main() {
|
||||
|
||||
logic.openProfile();
|
||||
|
||||
expect(logic.tab, 3);
|
||||
expect(logic.tab, 4);
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
@@ -159,10 +159,10 @@ void main() {
|
||||
expect(logic.tab, 1);
|
||||
|
||||
logic.myAction(MyPageAction.following);
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.tab, 3);
|
||||
|
||||
logic.openFollowingHistory();
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.tab, 3);
|
||||
expect(logic.followingHistoryRequest, 1);
|
||||
|
||||
logic.myAction(MyPageAction.resume);
|
||||
@@ -179,10 +179,10 @@ void main() {
|
||||
final logic = HomeLogic(dramas);
|
||||
|
||||
logic.discoverAction(DiscoverPageAction.mine);
|
||||
expect(logic.tab, 3);
|
||||
expect(logic.tab, 4);
|
||||
|
||||
logic.discoverAction(DiscoverPageAction.following);
|
||||
expect(logic.tab, 2);
|
||||
expect(logic.tab, 3);
|
||||
|
||||
logic.discoverAction(DiscoverPageAction.home);
|
||||
expect(logic.tab, 0);
|
||||
|
||||
+138
-64
@@ -3,10 +3,12 @@
|
||||
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';
|
||||
@@ -26,6 +28,7 @@ const _keys = [
|
||||
Key('home-tabs-bar'),
|
||||
Key('tab-home'),
|
||||
Key('tab-recommend'),
|
||||
Key('tab-reward'),
|
||||
Key('tab-follow'),
|
||||
Key('tab-mine'),
|
||||
];
|
||||
@@ -40,7 +43,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('should_theme_four_tabs_and_switch_selected_state_when_tapped', (
|
||||
testWidgets('should_theme_five_tabs_and_switch_selected_state_when_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
final haptics = FakeHaptics();
|
||||
@@ -83,6 +86,13 @@ void main() {
|
||||
.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')),
|
||||
@@ -90,6 +100,13 @@ void main() {
|
||||
),
|
||||
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')),
|
||||
@@ -128,17 +145,6 @@ void main() {
|
||||
?.color,
|
||||
AppColors.navUnselected,
|
||||
);
|
||||
expect(
|
||||
tester
|
||||
.widget<AnimatedScale>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
matching: find.byType(AnimatedScale),
|
||||
),
|
||||
)
|
||||
.duration,
|
||||
lessThanOrEqualTo(const Duration(milliseconds: 200)),
|
||||
);
|
||||
final selectedHomeIcon = tester.widget<RootNavGlyph>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('tab-home')),
|
||||
@@ -239,64 +245,72 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_map_four_tabs_to_home_recommend_following_and_me', (
|
||||
tester,
|
||||
) async {
|
||||
// 追剧页头部的双语分段文案在测试字体(Ahem)下比真机字体宽约 98pt,
|
||||
// 这里放宽视口宽度,避免测字体撑破 390 宽度下的分段行。
|
||||
await _pumpApp(tester, size: const Size(520, 844));
|
||||
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-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);
|
||||
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-recommend')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('recommend-channel-nav')), 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-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-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();
|
||||
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-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-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-home')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget);
|
||||
});
|
||||
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,
|
||||
@@ -337,6 +351,7 @@ Future<void> _pumpApp(
|
||||
commentRepository: FakeCommentRepository(),
|
||||
interactionRepository: FakeInteractionRepository(),
|
||||
commerceRepository: const _WalletCommerceRepository(),
|
||||
rewardRepository: const _RewardRepository(),
|
||||
haptics: haptics ?? FakeHaptics(),
|
||||
),
|
||||
);
|
||||
@@ -371,6 +386,65 @@ class _WalletCommerceRepository implements CommerceRepository {
|
||||
}
|
||||
}
|
||||
|
||||
class _RewardRepository implements RewardRepository {
|
||||
const _RewardRepository();
|
||||
|
||||
@override
|
||||
Future<RewardOverview> 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<RewardActionResult> checkIn() async {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _expectNoFloatingOverlays() {
|
||||
expect(find.byKey(const Key('mine-mini-player')), findsNothing);
|
||||
expect(find.byType(FloatingActionButton), findsNothing);
|
||||
|
||||
@@ -24,7 +24,7 @@ void main() {
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
});
|
||||
|
||||
testWidgets('should_render_four_tabs_and_open_following_page', (
|
||||
testWidgets('should_render_five_tabs_and_open_following_page', (
|
||||
tester,
|
||||
) async {
|
||||
// 追剧页头部双语分段在测试字体(Ahem)下比真机字体宽约 98pt,
|
||||
@@ -189,6 +189,7 @@ void main() {
|
||||
const _tabKeys = [
|
||||
Key('tab-home'),
|
||||
Key('tab-recommend'),
|
||||
Key('tab-reward'),
|
||||
Key('tab-follow'),
|
||||
Key('tab-mine'),
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'should_render_four_ordered_glyphs_with_home_selected_when_root_nav_builds',
|
||||
'should_render_five_ordered_glyphs_with_home_selected_when_root_nav_builds',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@@ -32,7 +32,19 @@ void main() {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
]);
|
||||
final tabKeys = [
|
||||
const Key('tab-home'),
|
||||
const Key('tab-recommend'),
|
||||
const Key('tab-reward'),
|
||||
const Key('tab-follow'),
|
||||
const Key('tab-mine'),
|
||||
];
|
||||
final centers = tabKeys
|
||||
.map((key) => tester.getCenter(find.byKey(key)).dx)
|
||||
.toList();
|
||||
expect(centers, orderedEquals(centers.toList()..sort()));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -35,13 +35,19 @@ void main() {
|
||||
Key('player-play-toggle'),
|
||||
]) {
|
||||
expect(tester.getSize(find.byKey(key)), const Size(68, 68));
|
||||
// 圆形深色底:压在亮画面上也能看见播放/暂停。
|
||||
final container = tester.widget<Container>(
|
||||
find.descendant(of: find.byKey(key), matching: find.byType(Container)),
|
||||
// 半透明玻璃圆面:压在亮画面上也能看见播放/暂停。
|
||||
final surface = tester.widget<DecoratedBox>(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byKey(key),
|
||||
matching: find.byKey(const Key('player-center-toggle-surface')),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
final decoration = container.decoration! as BoxDecoration;
|
||||
expect(decoration.color, const Color(0x66000000));
|
||||
final decoration = surface.decoration as BoxDecoration;
|
||||
expect(decoration.gradient, isA<RadialGradient>());
|
||||
expect(decoration.shape, BoxShape.circle);
|
||||
expect(decoration.border, isNull);
|
||||
final icon = tester.widget<Icon>(
|
||||
find.descendant(of: find.byKey(key), matching: find.byType(Icon)),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,60 @@ import '../support/fake_catalog.dart';
|
||||
import '../support/fake_category.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_keep_refresh_indicator_below_category_nav_when_pulling', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final controller = CategoryController(
|
||||
FakeCategoryRepository(catalog: FakeCatalogRepository()),
|
||||
);
|
||||
final dramas = [for (var index = 1; index <= 12; index++) _drama('$index')];
|
||||
controller.categories = const [ContentCategory(id: '7', name: 'Romance')];
|
||||
controller.dramaPageLoader =
|
||||
(categoryId, {pageIndex = 1, pageSize = 20}) async =>
|
||||
PageResult(total: dramas.length, list: dramas);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: ChangeNotifierProvider<CategoryController>.value(
|
||||
value: controller,
|
||||
child: const RecommendDiscoverPage(active: true, onSearch: _noop),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final nav = tester.getRect(find.byKey(const Key('recommend-tab-7')));
|
||||
final scroll = find.byKey(const PageStorageKey<String>('recommend-feed-7'));
|
||||
final scrollable = tester.state<ScrollableState>(
|
||||
find.descendant(of: scroll, matching: find.byType(Scrollable)).first,
|
||||
);
|
||||
expect(scrollable.position.pixels, 0);
|
||||
final scrollRect = tester.getRect(scroll);
|
||||
final gesture = await tester.startGesture(
|
||||
Offset(scrollRect.center.dx, scrollRect.top + 120),
|
||||
);
|
||||
await gesture.moveBy(const Offset(0, 160));
|
||||
await tester.pump();
|
||||
|
||||
final indicator = find.byKey(const Key('pull-refresh-indicator'));
|
||||
expect(indicator, findsOneWidget);
|
||||
expect(tester.getRect(indicator).top, greaterThanOrEqualTo(nav.bottom));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('should_center_empty_state_when_category_has_no_dramas', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/reward.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/reward_repository.dart';
|
||||
import 'package:fruitshorts/core/data/services/reward_controller.dart';
|
||||
|
||||
void main() {
|
||||
test('should_refresh_balance_when_check_in_succeeds', () async {
|
||||
final repository = _RewardRepository();
|
||||
final controller = RewardController(repository);
|
||||
|
||||
await controller.load('id');
|
||||
final result = await controller.checkIn();
|
||||
|
||||
expect(result?.rewardCoin, 35);
|
||||
expect(controller.overview?.coinBalance, 35);
|
||||
expect(repository.overviewCalls, 2);
|
||||
});
|
||||
|
||||
test('should_mark_claim_in_flight_when_task_is_claimed', () async {
|
||||
final repository = _RewardRepository();
|
||||
final controller = RewardController(repository);
|
||||
|
||||
final future = controller.claim('task-1');
|
||||
expect(controller.claimingTaskId, 'task-1');
|
||||
await future;
|
||||
|
||||
expect(controller.claimingTaskId, isNull);
|
||||
expect(repository.claimedTaskId, 'task-1');
|
||||
});
|
||||
}
|
||||
|
||||
class _RewardRepository implements RewardRepository {
|
||||
int overviewCalls = 0;
|
||||
String? claimedTaskId;
|
||||
|
||||
@override
|
||||
Future<RewardOverview> fetchOverview(String locale) async {
|
||||
overviewCalls += 1;
|
||||
return RewardOverview(
|
||||
serverDate: '2026-11-05',
|
||||
timezone: 'Asia/Jakarta',
|
||||
coinBalance: overviewCalls > 1 ? 35 : 0,
|
||||
checkin: const RewardCheckinState(
|
||||
taskId: '1',
|
||||
checkedInToday: false,
|
||||
streak: 0,
|
||||
currentDay: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RewardActionResult> checkIn() async {
|
||||
return const RewardActionResult(
|
||||
rewardCoin: 35,
|
||||
coinBalance: 35,
|
||||
alreadyCompleted: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RewardActionResult> claim(String taskId) async {
|
||||
claimedTaskId = taskId;
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
return const RewardActionResult(
|
||||
rewardCoin: 20,
|
||||
coinBalance: 55,
|
||||
alreadyCompleted: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/reward.dart';
|
||||
|
||||
void main() {
|
||||
test('should_parse_string_numbers_when_reward_overview_is_received', () {
|
||||
final overview = RewardOverview.fromJson({
|
||||
'serverDate': '2026-11-05',
|
||||
'timezone': 'Asia/Jakarta',
|
||||
'coinBalance': '135',
|
||||
'checkin': {
|
||||
'taskId': '9',
|
||||
'checkedInToday': true,
|
||||
'streak': 2,
|
||||
'currentDay': 3,
|
||||
'days': [
|
||||
{'dayIndex': 1, 'rewardCoin': '35', 'checked': true},
|
||||
],
|
||||
},
|
||||
'sections': [
|
||||
{
|
||||
'code': 'DAILY',
|
||||
'title': 'Tugas harian',
|
||||
'tasks': [
|
||||
{
|
||||
'id': '18',
|
||||
'targetValue': '300',
|
||||
'progressValue': '120',
|
||||
'rewardCoin': '20',
|
||||
'status': 'IN_PROGRESS',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(overview.coinBalance, 135);
|
||||
expect(overview.checkin.days.single.rewardCoin, 35);
|
||||
expect(overview.sections.single.tasks.single.progress, 0.4);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
// 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-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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
// 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/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/reward/reward_page.dart';
|
||||
|
||||
import '../support/widget_capture.dart';
|
||||
|
||||
const bool _captureEnabled = bool.fromEnvironment('REWARD_CAPTURE');
|
||||
const Size _screenSize = Size(393, 852);
|
||||
const Key _captureKey = Key('reward-visual-capture');
|
||||
const String _capturePath = '/tmp/fruitshorts-reward-before.png';
|
||||
const String _helpCapturePath = '/tmp/fruitshorts-reward-help-dialog.png';
|
||||
|
||||
void main() {
|
||||
testWidgets('should_render_reward_design_fixture_without_overflow', (
|
||||
tester,
|
||||
) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
await tester.binding.setSurfaceSize(_screenSize);
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
if (_captureEnabled) await loadCaptureFonts(tester);
|
||||
final auth = _VisualAuthController();
|
||||
final rewards = RewardController(_VisualRewardRepository());
|
||||
|
||||
await tester.pumpWidget(_host(auth: auth, rewards: rewards));
|
||||
await tester.pump(const Duration(milliseconds: 2));
|
||||
await tester.pumpAndSettle();
|
||||
if (_captureEnabled) {
|
||||
await saveWidgetCapture(tester, _captureKey, _capturePath);
|
||||
}
|
||||
|
||||
expect(find.byKey(const Key('reward-checkin-card')), findsOneWidget);
|
||||
expect(find.byKey(const Key('reward-task-card-INVITE')), findsOneWidget);
|
||||
expect(find.byKey(const Key('reward-task-card-NEWBIE')), findsOneWidget);
|
||||
expect(find.text('+0'), findsNothing);
|
||||
final action = tester.getSize(
|
||||
find.byKey(const Key('reward-checkin-action')),
|
||||
);
|
||||
final actionContent = tester.getSize(
|
||||
find.byKey(const Key('reward-checkin-action-content')),
|
||||
);
|
||||
expect(actionContent.width, lessThan(action.width * 0.8));
|
||||
expect(_radius(tester, const Key('reward-checkin-card')), 14);
|
||||
expect(_radius(tester, const Key('reward-task-surface-INVITE')), 14);
|
||||
final backIcon = tester.widget<Icon>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('reward-back-button')),
|
||||
matching: find.byIcon(Icons.arrow_back_ios_new_rounded),
|
||||
),
|
||||
);
|
||||
expect(backIcon.size, 16);
|
||||
final helpButton = tester.getSize(
|
||||
find.byKey(const Key('reward-help-icon')),
|
||||
);
|
||||
expect(helpButton.width, 24);
|
||||
expect(helpButton.height, 24);
|
||||
final helpHitTarget = tester.getSize(
|
||||
find.byKey(const Key('reward-help-button')),
|
||||
);
|
||||
expect(helpHitTarget.width, 48);
|
||||
expect(helpHitTarget.height, 48);
|
||||
final taskButton = tester.getSize(
|
||||
find.byKey(const Key('reward-task-action-invite')),
|
||||
);
|
||||
expect(taskButton.height, 36);
|
||||
expect(taskButton.width, 80);
|
||||
if (_captureEnabled) {
|
||||
await tester.tap(find.byKey(const Key('reward-help-button')));
|
||||
await tester.pumpAndSettle();
|
||||
await saveWidgetCapture(
|
||||
tester,
|
||||
const Key('reward-help-dialog'),
|
||||
_helpCapturePath,
|
||||
);
|
||||
await tester.tap(find.byKey(const Key('reward-help-close')));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
|
||||
auth.dispose();
|
||||
rewards.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
double _radius(WidgetTester tester, Key key) {
|
||||
final container = tester.widget<Container>(find.byKey(key));
|
||||
final decoration = container.decoration! as BoxDecoration;
|
||||
return decoration.borderRadius!.resolve(TextDirection.ltr).topLeft.x;
|
||||
}
|
||||
|
||||
Widget _host({
|
||||
required AuthController auth,
|
||||
required RewardController rewards,
|
||||
}) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<AuthController>.value(value: auth),
|
||||
ChangeNotifierProvider<RewardController>.value(value: rewards),
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: AppTheme.dark(),
|
||||
locale: const Locale('zh'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
size: _screenSize,
|
||||
padding: EdgeInsets.only(top: 24, bottom: 24),
|
||||
),
|
||||
child: const RepaintBoundary(
|
||||
key: _captureKey,
|
||||
child: Scaffold(body: RewardPage(active: true)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _VisualAuthController extends AuthController {
|
||||
_VisualAuthController()
|
||||
: super(
|
||||
AuthApi(ApiClient(baseUrl: 'http://127.0.0.1:8081')),
|
||||
AuthSessionStore(),
|
||||
);
|
||||
|
||||
@override
|
||||
bool get isSignedIn => false;
|
||||
}
|
||||
|
||||
class _VisualRewardRepository implements RewardRepository {
|
||||
@override
|
||||
Future<RewardOverview> fetchOverview(String locale) async {
|
||||
return const RewardOverview(
|
||||
serverDate: '2026-11-09',
|
||||
timezone: 'Asia/Jakarta',
|
||||
coinBalance: 0,
|
||||
checkin: RewardCheckinState(
|
||||
taskId: 'checkin',
|
||||
checkedInToday: false,
|
||||
streak: 0,
|
||||
currentDay: 1,
|
||||
days: [
|
||||
RewardCheckinDay(
|
||||
dayIndex: 1,
|
||||
rewardCoin: 55,
|
||||
checked: false,
|
||||
current: true,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 2,
|
||||
rewardCoin: 35,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 3,
|
||||
rewardCoin: 35,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 4,
|
||||
rewardCoin: 35,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 5,
|
||||
rewardCoin: 35,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 6,
|
||||
rewardCoin: 55,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
RewardCheckinDay(
|
||||
dayIndex: 7,
|
||||
rewardCoin: 55,
|
||||
checked: false,
|
||||
current: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
sections: [
|
||||
RewardSection(
|
||||
code: 'INVITE',
|
||||
title: '邀请任务',
|
||||
tasks: [
|
||||
RewardTask(
|
||||
id: 'invite',
|
||||
code: 'INVITE_FRIEND',
|
||||
type: 'INVITE_FRIEND',
|
||||
title: '领1小时免费看剧',
|
||||
subtitle: '邀请成功 +1小时',
|
||||
ctaText: '去完成',
|
||||
actionType: 'OPEN_URL',
|
||||
actionUrl: '',
|
||||
progressValue: 0,
|
||||
targetValue: 1,
|
||||
rewardCoin: 0,
|
||||
status: 'IN_PROGRESS',
|
||||
),
|
||||
],
|
||||
),
|
||||
RewardSection(
|
||||
code: 'NEWBIE',
|
||||
title: '新手专属任务',
|
||||
tasks: [
|
||||
RewardTask(
|
||||
id: 'facebook',
|
||||
code: 'FOLLOW_FACEBOOK',
|
||||
type: 'FOLLOW_SOCIAL',
|
||||
title: '关注 Facebook 账号',
|
||||
subtitle: '',
|
||||
ctaText: '去完成',
|
||||
actionType: 'OPEN_URL',
|
||||
actionUrl: '',
|
||||
progressValue: 0,
|
||||
targetValue: 1,
|
||||
rewardCoin: 50,
|
||||
status: 'IN_PROGRESS',
|
||||
),
|
||||
RewardTask(
|
||||
id: 'youtube',
|
||||
code: 'FOLLOW_YOUTUBE',
|
||||
type: 'FOLLOW_SOCIAL',
|
||||
title: '关注 YouTube 账号',
|
||||
subtitle: '',
|
||||
ctaText: '去完成',
|
||||
actionType: 'OPEN_URL',
|
||||
actionUrl: '',
|
||||
progressValue: 0,
|
||||
targetValue: 1,
|
||||
rewardCoin: 50,
|
||||
status: 'IN_PROGRESS',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RewardActionResult> checkIn() async {
|
||||
return const RewardActionResult(
|
||||
rewardCoin: 55,
|
||||
coinBalance: 55,
|
||||
alreadyCompleted: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RewardActionResult> claim(String taskId) async {
|
||||
return const RewardActionResult(
|
||||
rewardCoin: 50,
|
||||
coinBalance: 50,
|
||||
alreadyCompleted: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ void main() {
|
||||
test('should_expose_dark_theme_radius_and_spacing_scale', () {
|
||||
expect(AppDimens.pageHorizontal, 16);
|
||||
expect(AppDimens.cardRadius, 12);
|
||||
expect(AppDimens.coverRadius, 12);
|
||||
expect(AppDimens.coverRadius, 8);
|
||||
expect(AppDimens.cardCoverRadius, AppDimens.coverRadius);
|
||||
expect(AppDimens.rankingCoverRadius, AppDimens.coverRadius);
|
||||
expect(AppDimens.followingHistoryCoverRadius, AppDimens.coverRadius);
|
||||
expect(AppDimens.authCardRadius, 12);
|
||||
expect(AppDimens.buttonRadius, 10);
|
||||
expect(AppDimens.inputRadius, 10);
|
||||
|
||||
@@ -94,7 +94,7 @@ void main() {
|
||||
expect(find.byKey(const Key('player-controls-top')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_four_root_tabs_on_one_route_and_return_home', (
|
||||
testWidgets('should_keep_five_root_tabs_on_one_route_and_return_home', (
|
||||
tester,
|
||||
) async {
|
||||
// 追剧页头部双语分段在测试字体(Ahem)下更宽,放宽视口避免撑破分段行。
|
||||
@@ -269,9 +269,7 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_like_without_login_when_signed_out', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('should_like_without_login_when_signed_out', (tester) async {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
final interactions = FakeInteractionRepository();
|
||||
|
||||
Reference in new issue
Block a user