333 lines
11 KiB
Dart
333 lines
11 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) {
|
|
final context = tester.element(find.byKey(const Key('mine-vip-emblem')));
|
|
await tester.runAsync(() async {
|
|
await precacheImage(
|
|
const AssetImage('assets/icon/vip_membership_badge.png'),
|
|
context,
|
|
);
|
|
});
|
|
await tester.pumpAndSettle();
|
|
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,
|
|
);
|
|
}
|
|
}
|