// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/download_license.dart'; import 'package:fruitshorts/core/data/models/download_policy.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.dart'; import 'package:fruitshorts/core/data/repositories/download_repository.dart'; import 'package:fruitshorts/core/data/services/auth_controller.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart'; import 'package:fruitshorts/core/data/services/download_controller.dart'; import 'package:fruitshorts/core/platform/haptics.dart'; import 'package:fruitshorts/core/widgets/empty_state.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/downloads/downloads_page.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_http_adapter.dart'; import '../support/fake_haptics.dart'; DownloadPolicy _policy({bool wifiOnly = true, String maxQuality = 'AUTO'}) => DownloadPolicy( maxPerDevice: 20, licenseDays: 30, wifiOnly: wifiOnly, maxQuality: maxQuality, maxStorageBytes: 0, used: 1, remaining: 19, expirePolicyText: 'Licenses last 30 days', ); DownloadLicense _license(String episodeId) => DownloadLicense( licenseId: 'license-$episodeId', dramaId: 'drama-1', dramaTitle: 'The Signal', episodeId: episodeId, episodeNo: 2, deviceId: 'device-9', quality: '720P', playUrl: '', licenseKey: '', status: 'ACTIVE', grantedAt: DateTime(2026, 10, 17, 12), expireAt: DateTime(2026, 11, 16, 12), taskStatus: 'DOWNLOADING', progressPercent: 35, downloadedBytes: 1024, totalBytes: 4096, errorMsg: '', ); void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); testWidgets( 'should_render_policy_and_backend_status_values_when_page_loads', (tester) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository( policy: _policy(), licenses: [_license('1')], ); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); expect(find.byKey(const Key('downloads-page')), findsOneWidget); expect(find.byKey(const Key('download-policy')), findsOneWidget); expect(find.text('Licenses last 30 days'), findsOneWidget); expect(find.byKey(const Key('download-license-1')), findsOneWidget); expect(find.textContaining('ACTIVE'), findsOneWidget); expect(find.textContaining('DOWNLOADING'), findsOneWidget); expect(find.textContaining('35%'), findsOneWidget); }, ); testWidgets('should_show_empty_state_when_license_list_is_empty', ( tester, ) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository(policy: _policy()); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); expect(find.byKey(const Key('downloads-empty')), findsOneWidget); expect(find.byType(EmptyState), findsOneWidget); }); testWidgets('should_show_retry_when_policy_or_list_load_fails', ( tester, ) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository( policy: _policy(), failLoad: true, ); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); expect(find.byKey(const Key('downloads-error')), findsOneWidget); expect(find.byKey(const Key('downloads-retry')), findsOneWidget); }); testWidgets('should_show_auth_gate_without_loading_when_signed_out', ( tester, ) async { final auth = await _auth(signedIn: false); final repository = FakeDownloadRepository(policy: _policy()); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); expect(find.byKey(const Key('auth-gate-panel')), findsOneWidget); expect(repository.loadCalls, 0); }); testWidgets('should_update_wifi_preference_when_switch_is_tapped', ( tester, ) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository(policy: _policy()); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); await tester.tap(find.byKey(const Key('download-wifi-switch'))); await tester.pumpAndSettle(); expect(repository.settingsCalls, 1); expect(repository.lastWifiOnly, isFalse); }); testWidgets('should_clear_authorizations_only_after_confirmation', ( tester, ) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository( policy: _policy(), licenses: [_license('1')], ); final controller = DownloadController(repository); final haptics = FakeHaptics(); await _pumpPage( tester, auth: auth, controller: controller, haptics: haptics, ); await tester.tap(find.byKey(const Key('download-clear'))); await tester.pumpAndSettle(); expect(repository.clearCalls, 0); await tester.tap(find.widgetWithText(FilledButton, 'Clear')); await tester.pumpAndSettle(); expect(repository.clearCalls, 1); expect(haptics.mediumImpacts, 1); expect(find.byKey(const Key('downloads-empty')), findsOneWidget); expect(find.byType(EmptyState), findsOneWidget); }); testWidgets( 'should_render_unique_quality_options_when_policy_uses_a_preset', (tester) async { final auth = await _auth(signedIn: true); final repository = FakeDownloadRepository( policy: _policy(maxQuality: '720P'), ); final controller = DownloadController(repository); await _pumpPage(tester, auth: auth, controller: controller); expect(tester.takeException(), isNull); expect(find.byKey(const Key('download-quality')), findsOneWidget); }, ); } Future _auth({required bool signedIn}) async { if (signedIn) { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({ 'auth.accessToken': 'test-access', 'auth.refreshToken': 'test-refresh', 'auth.user': '{"uid":"1","nickname":"Viewer","avatarUrl":""}', }); } final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: FakeHttpAdapter( (_) async => FakeHttpResponse('{"code":0,"message":"ok","data":null}'), ), ); final auth = AuthController(AuthApi(client), AuthSessionStore()); await auth.load(); return auth; } Future _pumpPage( WidgetTester tester, { required AuthController auth, required DownloadController controller, FakeHaptics? haptics, }) async { await tester.pumpWidget( MultiProvider( providers: [ ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider.value(value: controller), Provider.value(value: haptics ?? FakeHaptics()), ], child: MaterialApp( locale: const Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const DownloadsPage(), ), ), ); await tester.pumpAndSettle(); } class FakeDownloadRepository implements DownloadRepository { FakeDownloadRepository({ required this.policy, this.licenses = const [], this.failLoad = false, }); DownloadPolicy policy; List licenses; bool failLoad; int loadCalls = 0; int settingsCalls = 0; int clearCalls = 0; bool? lastWifiOnly; @override Future fetchPolicy() async { loadCalls++; if (failLoad) throw StateError('offline'); return policy; } @override Future> list() async { if (failLoad) throw StateError('offline'); return licenses; } @override Future grant(String episodeId, {String? quality}) async => _license(episodeId); @override Future updateSettings({bool? wifiOnly, String? maxQuality}) async { settingsCalls++; lastWifiOnly = wifiOnly; } @override Future refreshUrl(String episodeId) async => _license(episodeId); @override Future reportProgress( String episodeId, { String? deviceId, String? status, int? progressPercent, int? downloadedBytes, int? totalBytes, String? errorMsg, }) async => _license(episodeId); @override Future remove(String episodeId) async { licenses = licenses.where((item) => item.episodeId != episodeId).toList(); } @override Future clear() async { clearCalls++; final count = licenses.length; licenses = const []; return count; } }