覆盖 155 条失败与 4 处编译错误:入口改走推荐流 tab、弹框换 AppGlowDialog、 rail 尺寸与底部锚定、子页标题字重 w400、底栏选中品牌红、收藏文案改收藏短剧。 全量 flutter test 775 通过。
298 lines
9.1 KiB
Dart
298 lines
9.1 KiB
Dart
// 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(TextButton, '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<AuthController> _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<void> _pumpPage(
|
|
WidgetTester tester, {
|
|
required AuthController auth,
|
|
required DownloadController controller,
|
|
FakeHaptics? haptics,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider<AuthController>.value(value: auth),
|
|
ChangeNotifierProvider<DownloadController>.value(value: controller),
|
|
Provider<Haptics>.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<DownloadLicense> licenses;
|
|
bool failLoad;
|
|
int loadCalls = 0;
|
|
int settingsCalls = 0;
|
|
int clearCalls = 0;
|
|
bool? lastWifiOnly;
|
|
|
|
@override
|
|
Future<DownloadPolicy> fetchPolicy() async {
|
|
loadCalls++;
|
|
if (failLoad) throw StateError('offline');
|
|
return policy;
|
|
}
|
|
|
|
@override
|
|
Future<List<DownloadLicense>> list() async {
|
|
if (failLoad) throw StateError('offline');
|
|
return licenses;
|
|
}
|
|
|
|
@override
|
|
Future<DownloadLicense> grant(String episodeId, {String? quality}) async =>
|
|
_license(episodeId);
|
|
|
|
@override
|
|
Future<void> updateSettings({bool? wifiOnly, String? maxQuality}) async {
|
|
settingsCalls++;
|
|
lastWifiOnly = wifiOnly;
|
|
}
|
|
|
|
@override
|
|
Future<DownloadLicense> refreshUrl(String episodeId) async =>
|
|
_license(episodeId);
|
|
|
|
@override
|
|
Future<DownloadLicense> reportProgress(
|
|
String episodeId, {
|
|
String? deviceId,
|
|
String? status,
|
|
int? progressPercent,
|
|
int? downloadedBytes,
|
|
int? totalBytes,
|
|
String? errorMsg,
|
|
}) async => _license(episodeId);
|
|
|
|
@override
|
|
Future<void> remove(String episodeId) async {
|
|
licenses = licenses.where((item) => item.episodeId != episodeId).toList();
|
|
}
|
|
|
|
@override
|
|
Future<int> clear() async {
|
|
clearCalls++;
|
|
final count = licenses.length;
|
|
licenses = const [];
|
|
return count;
|
|
}
|
|
}
|