// 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(); 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(); 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).value}-value'); return tester.widget(find.byKey(valueKey)).data; } Future _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? gate; @override Future> 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? gate; @override Future> 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? gate; @override Future 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 unlock(String episodeId) async { return UnlockResult( unlocked: true, accessType: 'UNLOCKED', coinCost: 0, coinBalance: balance, ); } }