feat(mine): 重做我的页会员卡与功能宫格布局
This commit is contained in:
1 parent
e7399fa3fd
commit
53e8c924a6
12 files changed
+1969
-513
No files matched your search
@@ -0,0 +1,324 @@
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user