Files
ReelShort/test/mine/mine_stats_data_test.dart

325 lines
10 KiB
Dart

// Author: fruit.com.cn
// 验证我的页个人数据的真实总数、未知状态、导航与刷新。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/app.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/models/page_result.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/data/services/auth_controller.dart';
import 'package:fruitshorts/pages/auth/auth_view.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 '../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';
import '../support/widget_capture.dart';
const _coins = Key('mine-stat-coins');
const _history = Key('mine-stat-watch-records');
const _favorites = Key('mine-stat-favorites');
const _stats = Key('mine-stats');
const _screenSize = Size(390, 844);
const _initialBalance = 320;
const _historyTotal = 57;
const _favoritesTotal = 23;
const _updatedBalance = 270;
const _updatedHistoryTotal = 58;
const _updatedFavoritesTotal = 24;
const _unavailable = '—';
const _captureEnabled = bool.fromEnvironment('MINE_STATS_CAPTURE');
const _captureKey = Key('mine-stats-capture');
const _capturePath = '/tmp/fruitshorts-mine-personal-stats.png';
void main() {
testWidgets('should_place_three_live_totals_above_vip_when_mine_opens', (
tester,
) async {
if (_captureEnabled) await loadCaptureFonts(tester);
await _pumpFixture(tester, _StatsFixture());
expect(find.byKey(_stats), findsOneWidget);
expect(_value(tester, _coins), '320');
expect(_value(tester, _history), '57');
expect(_value(tester, _favorites), '23');
final order = [
tester.getBottomLeft(find.byKey(const Key('mine-user-row'))).dy,
tester.getTopLeft(find.byKey(_stats)).dy,
tester.getBottomLeft(find.byKey(_stats)).dy,
tester.getTopLeft(find.byKey(const Key('mine-vip'))).dy,
];
expect(order, orderedEquals([...order]..sort()));
final tops = [
_coins,
_history,
_favorites,
].map((key) => tester.getTopLeft(find.byKey(key)).dy);
expect(tops.toSet(), hasLength(1));
expect(tester.takeException(), isNull);
if (_captureEnabled) {
await saveWidgetCapture(tester, _captureKey, _capturePath);
}
});
testWidgets('should_keep_device_counts_but_hide_balance_when_guest', (
tester,
) async {
final fixture = _StatsFixture();
await _pumpFixture(tester, fixture, signedIn: false);
expect(_value(tester, _coins), _unavailable);
expect(_value(tester, _history), '57');
expect(_value(tester, _favorites), '23');
expect(fixture.commerce.loads, 0);
await tester.tap(find.byKey(_coins));
await tester.pumpAndSettle();
expect(find.byKey(const Key('wallet-page')), findsNothing);
await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();
expect(find.byType(AuthPage), findsOneWidget);
expect(find.byKey(const Key('wallet-page')), findsNothing);
});
testWidgets('should_use_placeholders_until_actual_zero_totals_load', (
tester,
) async {
final gate = Completer<void>();
final fixture = _StatsFixture()
..history.gate = gate
..favorites.gate = gate
..commerce.gate = gate
..history.total = 0
..favorites.total = 0
..commerce.balance = 0;
await _pumpFixture(tester, fixture);
for (final key in [_coins, _history, _favorites]) {
expect(_value(tester, key), _unavailable);
}
gate.complete();
await tester.pumpAndSettle();
for (final key in [_coins, _history, _favorites]) {
expect(_value(tester, key), '0');
}
});
testWidgets('should_not_display_fake_zero_when_data_requests_fail', (
tester,
) async {
final fixture = _StatsFixture()
..history.failRecent = true
..favorites.failPage = true
..commerce.failLoad = true;
await _pumpFixture(tester, fixture);
for (final key in [_coins, _history, _favorites]) {
expect(_value(tester, key), _unavailable);
}
expect(tester.takeException(), isNull);
});
testWidgets('should_open_existing_destinations_when_statistics_are_tapped', (
tester,
) async {
await _pumpFixture(tester, _StatsFixture());
const destinations = [
(_coins, Key('wallet-page')),
(_history, Key('watch-records-page')),
(_favorites, Key('favorites-page')),
];
for (final destination in destinations) {
await tester.tap(find.byKey(destination.$1));
await tester.pumpAndSettle();
expect(find.byKey(destination.$2), findsOneWidget);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
}
expect(tester.takeException(), isNull);
});
testWidgets('should_refresh_totals_when_mine_becomes_visible_again', (
tester,
) async {
final fixture = _StatsFixture();
await _pumpFixture(tester, fixture);
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
fixture.history.total = _updatedHistoryTotal;
fixture.favorites.total = _updatedFavoritesTotal;
fixture.commerce.balance = _updatedBalance;
await tester.tap(find.byKey(const Key('tab-mine')));
await tester.pumpAndSettle();
expect(_value(tester, _coins), '270');
expect(_value(tester, _history), '58');
expect(_value(tester, _favorites), '24');
});
testWidgets('should_refresh_favorites_when_returning_from_favorites_page', (
tester,
) async {
final fixture = _StatsFixture();
await _pumpFixture(tester, fixture);
await tester.tap(find.byKey(_favorites));
await tester.pumpAndSettle();
fixture.favorites.total = _updatedFavoritesTotal;
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
expect(_value(tester, _favorites), '24');
});
testWidgets('should_hide_previous_balance_immediately_when_signed_out', (
tester,
) async {
await _pumpFixture(tester, _StatsFixture());
final auth = tester.element(find.byKey(_stats)).read<AuthController>();
await auth.clear();
await tester.pumpAndSettle();
expect(_value(tester, _coins), _unavailable);
expect(find.byKey(const Key('mine-login')), findsOneWidget);
});
}
String? _value(WidgetTester tester, Key key) {
final valueKey = Key('${(key as ValueKey<String>).value}-value');
return tester.widget<Text>(find.byKey(valueKey)).data;
}
Future<void> _pumpFixture(
WidgetTester tester,
_StatsFixture fixture, {
bool signedIn = true,
}) async {
tester.view.physicalSize = _screenSize;
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(
RepaintBoundary(
key: _captureKey,
child: LumaDramaApp(
playerFactory: createWidgetPlayer,
catalogRepository: catalog,
categoryRepository: FakeCategoryRepository(catalog: catalog),
watchHistoryRepository: fixture.history,
favoriteRepository: fixture.favorites,
commerceRepository: fixture.commerce,
playbackRepository: FakePlaybackRepository(),
commentRepository: FakeCommentRepository(),
interactionRepository: FakeInteractionRepository(),
noticeRepository: FakeNoticeRepository(),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('tab-mine')));
await tester.pumpAndSettle();
}
class _StatsFixture {
final history = _StatsHistory();
final favorites = _StatsFavorites();
final commerce = _StatsCommerce();
}
class _StatsHistory extends InMemoryWatchHistoryRepository {
int total = _historyTotal;
bool failRecent = false;
Completer<void>? gate;
@override
Future<PageResult<WatchProgress>> recent({
int pageIndex = 1,
int pageSize = 20,
}) async {
if (gate != null) await gate!.future;
if (failRecent) throw StateError('history unavailable');
return PageResult(
total: total,
list: total == 0
? const []
: const [
WatchProgress(
dramaId: 'the-signal-demo',
episodeId: 'the-signal-demo-1',
position: Duration.zero,
duration: Duration(minutes: 1),
),
],
);
}
}
class _StatsFavorites extends InMemoryFavoriteRepository {
int total = _favoritesTotal;
Completer<void>? gate;
@override
Future<PageResult<Drama>> page({int pageIndex = 1, int pageSize = 20}) async {
if (gate != null) await gate!.future;
if (failPage) throw StateError('favorites unavailable');
return PageResult(
total: total,
list: total == 0 ? const [] : demoDramas.take(1).toList(),
);
}
}
class _StatsCommerce implements CommerceRepository {
int balance = _initialBalance;
int loads = 0;
bool failLoad = false;
Completer<void>? gate;
@override
Future<WalletSnapshot> fetchWallet() async {
loads += 1;
if (gate != null) await gate!.future;
if (failLoad) throw StateError('wallet unavailable');
return WalletSnapshot(
wallet: CoinWallet(
coinBalance: balance,
totalRechargedCoin: balance,
totalConsumedCoin: 0,
totalRefundedCoin: 0,
),
);
}
@override
Future<UnlockResult> unlock(String episodeId) async {
return UnlockResult(
unlocked: true,
accessType: 'UNLOCKED',
coinCost: 0,
coinBalance: balance,
);
}
}