Files
ReelShort/test/mine/mine_preview_layout_test.dart

460 lines
16 KiB
Dart

// 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_action_grid.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_header_vip_quick_actions_and_services', (
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 nickname = tester.widget<Text>(
find.byKey(const Key('mine-nickname')),
);
expect(nickname.style?.fontSize, AppDimens.mineUserName);
expect(nickname.style?.fontWeight, FontWeight.w400);
expect(nickname.style?.height, AppDimens.mineUserNameLineHeight);
final uid = tester.widget<Text>(find.text('UID: --'));
expect(uid.style?.fontSize, AppDimens.mineUid);
expect(uid.style?.color, AppColors.textTertiary);
final copyIcon = tester.widget<Icon>(
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-mini-player')), findsNothing);
final surface = tester.widget<DecoratedBox>(
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.followingGlow,
AppColors.followingGlowMid,
AppColors.appBackground,
]);
expect(background.stops, const [0, 0.14, 0.3]);
// 统一操作宫格第一排:钱包 / 收藏 / 离线缓存 / 观看历史。
const quickActions = [
(Key('mine-wallet'), Icons.account_balance_wallet_outlined),
(Key('mine-favorites'), Icons.star_outline_rounded),
(Key('mine-downloads'), Icons.download_for_offline_outlined),
(Key('mine-history'), Icons.history_rounded),
];
for (final action in quickActions) {
final icon = tester.widget<Icon>(
find.descendant(
of: find.byKey(action.$1),
matching: find.byIcon(action.$2),
),
);
expect(icon.color, AppColors.mineGridIconAccent);
expect(icon.size, AppDimens.mineGridIcon);
expect(
find.descendant(
of: find.byKey(const Key('mine-menu')),
matching: find.byKey(action.$1),
),
findsOneWidget,
);
}
// 「更多服务」宫格:联系客服 / 语言 / 设置 / 通知。
const services = [
(
Key('mine-help'),
Key('mine-help-icon-surface'),
Icons.support_agent_outlined,
),
(
Key('mine-language'),
Key('mine-language-icon-surface'),
Icons.language_rounded,
),
(
Key('mine-settings'),
Key('mine-settings-icon-surface'),
Icons.settings_outlined,
),
(
Key('mine-notices'),
Key('mine-notices-icon-surface'),
Icons.notifications_none_rounded,
),
];
for (final service in services) {
expect(find.byKey(service.$1), findsOneWidget);
final icon = tester.widget<Icon>(
find.descendant(
of: find.byKey(service.$1),
matching: find.byIcon(service.$3),
),
);
expect(icon.color, AppColors.mineGridIconAccent);
expect(icon.size, AppDimens.mineGridIcon);
expect(
tester.getSize(find.byKey(service.$2)),
const Size.square(AppDimens.mineGridIconSurface),
);
}
expect(find.byKey(const Key('mine-menu')), findsOneWidget);
expect(tester.getTopLeft(find.byKey(const Key('mine-menu'))).dx, 14);
expect(
tester.getSize(find.byKey(const Key('mine-menu'))).height,
AppDimens.mineGridHeight,
);
final serviceLefts = services
.map((service) => tester.getTopLeft(find.byKey(service.$1)).dx)
.toList();
expect(serviceLefts, [...serviceLefts]..sort());
expect(serviceLefts.toSet(), hasLength(services.length));
expect(
tester.getTopLeft(find.byKey(const Key('mine-help'))).dy,
greaterThan(tester.getTopLeft(find.byKey(const Key('mine-wallet'))).dy),
);
final menu = tester.widget<DecoratedBox>(
find.byKey(const Key('mine-menu')),
);
final menuDecoration = menu.decoration as BoxDecoration;
expect(menuDecoration.color, AppColors.mineGridSurface);
expect(
menuDecoration.borderRadius,
BorderRadius.circular(AppDimens.mineGridRadius),
);
final vip = tester.widget<Container>(find.byKey(const Key('mine-vip')));
final vipDecoration = vip.decoration! as BoxDecoration;
expect((vipDecoration.gradient! as LinearGradient).colors, const [
AppColors.vipCardStart,
AppColors.vipCardMiddle,
AppColors.vipCardEnd,
]);
expect(vipDecoration.border, isNotNull);
expect(vipDecoration.boxShadow, isNotEmpty);
// 卡片高度由内容撑开,上下内边距必须一致(边框各占 1pt)。
final vipRect = tester.getRect(find.byKey(const Key('mine-vip')));
final emblemRect = tester.getRect(find.byKey(const Key('mine-vip-emblem')));
final benefitsRect = tester.getRect(
find.byKey(const Key('mine-vip-benefits')),
);
final topInset = emblemRect.top - vipRect.top;
final bottomInset = vipRect.bottom - benefitsRect.bottom;
expect(topInset, closeTo(bottomInset, 0.01));
expect(topInset, greaterThanOrEqualTo(AppDimens.mineVipPadding));
expect(find.byKey(const Key('mine-vip-emblem')), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('mine-vip-open'))).height,
greaterThanOrEqualTo(AppDimens.tapTarget),
);
const benefitKeys = [
Key('mine-vip-benefit-0'),
Key('mine-vip-benefit-1'),
Key('mine-vip-benefit-2'),
Key('mine-vip-benefit-3'),
];
final benefitTops = benefitKeys
.map((key) => tester.getTopLeft(find.byKey(key)))
.toList();
expect(benefitTops.map((position) => position.dy).toSet(), hasLength(1));
expect(
benefitTops.map((position) => position.dx).toSet(),
hasLength(benefitKeys.length),
);
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-wallet'))).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-notices')), findsOneWidget);
await tester.tap(find.byKey(const Key('mine-edit')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('account-profile-page')), findsOneWidget);
expect(tester.takeException(), isNull);
});
testWidgets('should_open_help_and_about_from_mine', (tester) async {
await _pumpApp(tester, signedIn: false);
await _openMine(tester);
await tester.tap(find.byKey(const Key('mine-help')));
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-settings')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('settings-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('Your mini business'), findsOneWidget);
expect(
find.text(
AppLocalizations.of(
tester.element(find.byType(MineAboutPage)),
)!.aboutDescription,
),
findsOneWidget,
);
});
testWidgets('should_open_watch_records_from_quick_actions', (tester) async {
await _pumpApp(
tester,
signedIn: true,
history: InMemoryWatchHistoryRepository(seed: [_recent]),
);
await _openMine(tester);
await tester.tap(find.byKey(const Key('mine-history')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('watch-records-page')), 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_quick_actions_and_service_destinations', (
tester,
) async {
tester.view.physicalSize = const Size(390, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final opened = <MineDestination>[];
var languageOpened = 0;
var favoritesOpened = 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: Column(
children: [
MineActionGrid(
primaryActions: MinePrimaryActions(
onWallet: () => opened.add(MineDestination.wallet),
onFavorites: () => favoritesOpened++,
onDownloads: () => opened.add(MineDestination.downloads),
onHistory: () => opened.add(MineDestination.history),
),
serviceActions: MineServiceActions(
onOpen: opened.add,
onLanguage: () => languageOpened++,
),
),
],
),
),
),
),
);
const quickActions = [
(Key('mine-wallet'), MineDestination.wallet),
(Key('mine-downloads'), MineDestination.downloads),
(Key('mine-history'), MineDestination.history),
];
for (final action in quickActions) {
await tester.tap(find.byKey(action.$1));
await tester.pump();
expect(opened.last, action.$2);
}
await tester.tap(find.byKey(const Key('mine-favorites')));
await tester.pump();
expect(favoritesOpened, 1);
const services = [
(Key('mine-help'), MineDestination.help),
(Key('mine-notices'), MineDestination.notices),
(Key('mine-settings'), MineDestination.settings),
];
for (final service in services) {
await tester.tap(find.byKey(service.$1));
await tester.pump();
expect(opened.last, service.$2);
}
await tester.tap(find.byKey(const Key('mine-language')));
await tester.pump();
expect(languageOpened, 1);
});
}
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<void> _openMine(WidgetTester tester) async {
await tester.tap(find.byKey(const Key('tab-mine')));
await tester.pumpAndSettle();
}
Future<void> _pumpApp(
WidgetTester tester, {
required bool signedIn,
WatchHistoryRepository? history,
FavoriteRepository? favorite,
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:
favorite ??
InMemoryFavoriteRepository(
saved: catalog.dramas.map((drama) => drama.id).toSet(),
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<WalletSnapshot> fetchWallet() async {
return const WalletSnapshot(
wallet: CoinWallet(
coinBalance: 320,
totalRechargedCoin: 500,
totalConsumedCoin: 180,
totalRefundedCoin: 0,
),
);
}
@override
Future<UnlockResult> unlock(String episodeId) async {
return const UnlockResult(
unlocked: true,
accessType: 'UNLOCKED',
coinCost: 0,
coinBalance: 320,
);
}
}