280 lines
8.6 KiB
Dart
280 lines
8.6 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/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,
|
|
);
|
|
}
|
|
}
|