mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 04:13:43 +00:00
refactor: remove proxy API (#1596)
This commit is contained in:
98 files changed
+5958
-1225
No files matched your search
@@ -1,4 +1,3 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/api/api.dart' as api;
|
||||
import 'package:gopeed/core/network/gopeed/gopeed_transport.dart';
|
||||
@@ -31,11 +30,6 @@ class _LoginTransport implements GopeedTransport {
|
||||
return {'code': 0};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response<String>> proxyRequest(String uri, {dynamic data, Options? options}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String join(String path) => path;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/api/model/downloader_config.dart';
|
||||
@@ -67,6 +69,106 @@ void main() {
|
||||
expect(saved?.extra.createHistory, ['https://example.com/existing.zip']);
|
||||
expect(saved?.extra.analyticsClientId, 'existing-client');
|
||||
});
|
||||
|
||||
test('protocol edits use a single full PUT and reload backend-owned tracker results', () async {
|
||||
final backend = DownloaderConfig();
|
||||
final puts = <DownloaderConfig>[];
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig.fromJson(backend.toJson()))
|
||||
..bind(GopeedMethods.putConfig, (config) {
|
||||
puts.add(config);
|
||||
// Simulate the backend retaining its read-only results on a full PUT.
|
||||
backend.protocolConfig.bt.listenPort = config.protocolConfig.bt.listenPort;
|
||||
return const RpcUnit();
|
||||
});
|
||||
final container = _settingsContainer(registry);
|
||||
addTearDown(container.dispose);
|
||||
final initial = await container.read(settingsControllerProvider.future);
|
||||
final draft = DownloaderConfig.fromJson(initial.config.toJson())..protocolConfig.bt.listenPort = 6881;
|
||||
backend.protocolConfig.bt.state.trackerSources = {
|
||||
'https://example.com/list': ['udp://fresh.example:80/announce'],
|
||||
};
|
||||
backend.protocolConfig.bt.state.lastTrackerUpdateTime = DateTime.utc(2026, 10, 19);
|
||||
await container.read(settingsControllerProvider.notifier).save(draft);
|
||||
expect(puts, hasLength(1));
|
||||
expect(puts.single.protocolConfig.bt.listenPort, 6881);
|
||||
final saved = container.read(settingsControllerProvider).requireValue;
|
||||
expect(saved.config.protocolConfig.bt.state.trackerSources['https://example.com/list'], [
|
||||
'udp://fresh.example:80/announce',
|
||||
]);
|
||||
expect(saved.saving, isFalse);
|
||||
});
|
||||
|
||||
test('rapid full config saves run in order and keep the latest user edits', () async {
|
||||
final backend = DownloaderConfig();
|
||||
final entered = Completer<void>();
|
||||
final release = Completer<void>();
|
||||
final puts = <DownloaderConfig>[];
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig.fromJson(backend.toJson()))
|
||||
..bind(GopeedMethods.putConfig, (config) async {
|
||||
puts.add(DownloaderConfig.fromJson(config.toJson()));
|
||||
if (puts.length == 1) {
|
||||
entered.complete();
|
||||
await release.future;
|
||||
}
|
||||
backend.protocolConfig = config.protocolConfig;
|
||||
return const RpcUnit();
|
||||
});
|
||||
final container = _settingsContainer(registry);
|
||||
addTearDown(container.dispose);
|
||||
final initial = await container.read(settingsControllerProvider.future);
|
||||
final controller = container.read(settingsControllerProvider.notifier);
|
||||
final first = DownloaderConfig.fromJson(initial.config.toJson())..protocolConfig.bt.listenPort = 6881;
|
||||
final savingFirst = controller.save(first);
|
||||
await entered.future;
|
||||
final second = DownloaderConfig.fromJson(first.toJson())
|
||||
..protocolConfig.bt.customTrackers = ['udp://custom.example:80/announce'];
|
||||
final savingSecond = controller.save(second);
|
||||
expect(puts.length, 1);
|
||||
release.complete();
|
||||
await Future.wait([savingFirst, savingSecond]);
|
||||
expect(puts, hasLength(2));
|
||||
expect(puts[0].protocolConfig.bt.listenPort, 6881);
|
||||
expect(puts[1].protocolConfig.bt.listenPort, 6881);
|
||||
expect(puts[1].protocolConfig.bt.customTrackers, ['udp://custom.example:80/announce']);
|
||||
expect(container.read(settingsControllerProvider).requireValue.config.protocolConfig.bt.customTrackers, [
|
||||
'udp://custom.example:80/announce',
|
||||
]);
|
||||
});
|
||||
|
||||
test('a failed save clears saving state and the same change can be retried', () async {
|
||||
final backend = DownloaderConfig();
|
||||
var attempts = 0;
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig.fromJson(backend.toJson()))
|
||||
..bind(GopeedMethods.putConfig, (config) {
|
||||
if (++attempts == 1) throw StateError('offline');
|
||||
backend.protocolConfig = config.protocolConfig;
|
||||
return const RpcUnit();
|
||||
});
|
||||
final container = _settingsContainer(registry);
|
||||
addTearDown(container.dispose);
|
||||
final initial = await container.read(settingsControllerProvider.future);
|
||||
final controller = container.read(settingsControllerProvider.notifier);
|
||||
final draft = DownloaderConfig.fromJson(initial.config.toJson())..protocolConfig.bt.listenPort = 6881;
|
||||
await expectLater(controller.save(draft), throwsStateError);
|
||||
expect(container.read(settingsControllerProvider).requireValue.saving, isFalse);
|
||||
await controller.save(draft);
|
||||
expect(attempts, 2);
|
||||
expect(backend.protocolConfig.bt.listenPort, 6881);
|
||||
});
|
||||
}
|
||||
|
||||
ProviderContainer _settingsContainer(CapabilityRegistry registry) {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
appCapabilitiesProvider.overrideWithValue(AppCapabilities(LocalCapabilityInvoker(registry))),
|
||||
appRuntimeControllerProvider.overrideWith(_TestRuntimeController.new),
|
||||
],
|
||||
);
|
||||
container.listen(settingsControllerProvider, (_, _) {});
|
||||
return container;
|
||||
}
|
||||
|
||||
class _TestRuntimeController extends AppRuntimeController {
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter/gestures.dart' show PointerDeviceKind;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:gopeed/api/model/downloader_config.dart';
|
||||
import 'package:gopeed/features/settings/presentation/widgets/tracker_subscriptions_control.dart';
|
||||
import 'package:gopeed/features/settings/presentation/widgets/tracker_subscriptions_schedule.dart';
|
||||
import 'package:gopeed/features/settings/presentation/widgets/tracker_subscriptions_table.dart';
|
||||
import 'package:gopeed/l10n/l10n.dart';
|
||||
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
for (final width in [360.0, 768.0, 1024.0]) {
|
||||
testWidgets('subscription results fit at width $width', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final time = DateTime.utc(2026, 10, 19);
|
||||
await tester.pumpWidget(
|
||||
ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: AppComponentThemes(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TrackerSubscriptionsControl(
|
||||
urls: const [
|
||||
'https://success.example/very/long/path/to/my/custom/list.txt',
|
||||
'https://failed.example/list',
|
||||
'https://new.example/list',
|
||||
],
|
||||
statuses: {
|
||||
'https://success.example/very/long/path/to/my/custom/list.txt': TrackerSubscriptionStatus(
|
||||
success: true,
|
||||
trackerCount: 7,
|
||||
lastFetchTime: time,
|
||||
),
|
||||
'https://failed.example/list': TrackerSubscriptionStatus(
|
||||
success: false,
|
||||
trackerCount: 0,
|
||||
lastFetchTime: time,
|
||||
),
|
||||
},
|
||||
autoUpdate: true,
|
||||
lastUpdated: time,
|
||||
onChanged: (_) {},
|
||||
onAutoUpdateChanged: (_) {},
|
||||
onUpdate: () async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Update status'), findsOneWidget);
|
||||
expect(find.text('Tracker count'), findsOneWidget);
|
||||
expect(find.text('Actions'), findsOneWidget);
|
||||
for (final entry in const [
|
||||
('https://success.example/very/long/path/to/my/custom/list.txt', 'Succeeded', '7'),
|
||||
('https://failed.example/list', 'Failed', '0'),
|
||||
('https://new.example/list', 'Not updated', '—'),
|
||||
]) {
|
||||
final row = find.byKey(ValueKey('tracker-subscription-${entry.$1}'));
|
||||
expect(find.descendant(of: row, matching: find.text(entry.$2)), findsOneWidget);
|
||||
expect(find.descendant(of: row, matching: find.text(entry.$3)), findsOneWidget);
|
||||
}
|
||||
final table = find.byKey(const ValueKey('tracker-subscriptions-table'));
|
||||
expect(find.descendant(of: table, matching: find.textContaining('2026-')), findsNothing);
|
||||
expect(find.textContaining('Last updated: '), findsOneWidget);
|
||||
final updateRect = tester.getRect(find.byKey(const ValueKey('tracker-update-button')));
|
||||
final addRect = tester.getRect(find.byKey(const ValueKey('add-tracker-subscription')));
|
||||
final tableRect = tester.getRect(table);
|
||||
final autoUpdateRect = tester.getRect(find.byKey(const ValueKey('tracker-auto-update-control')));
|
||||
final timeRect = tester.getRect(find.byKey(const ValueKey('tracker-last-updated')));
|
||||
expect(addRect.right, closeTo(tableRect.right, 1));
|
||||
expect(updateRect.right, lessThan(addRect.left));
|
||||
expect(addRect.center.dy, closeTo(updateRect.center.dy, 1));
|
||||
expect(updateRect.top, greaterThan(tableRect.bottom));
|
||||
expect(autoUpdateRect.bottom, lessThan(tableRect.top));
|
||||
expect(autoUpdateRect.right, closeTo(tableRect.right, 1));
|
||||
expect(timeRect.top, greaterThan(autoUpdateRect.bottom));
|
||||
expect(timeRect.bottom, lessThan(tableRect.top));
|
||||
expect(timeRect.right, closeTo(tableRect.right, 1));
|
||||
expect(find.byKey(const ValueKey('add-tracker-subscription')), findsOneWidget);
|
||||
if (width < 672) {
|
||||
final headerPosition = tester.getTopLeft(find.text('Subscription URL'));
|
||||
await tester.dragFrom(tester.getTopLeft(table) + const Offset(100, 20), const Offset(-600, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.text('Subscription URL')).dx, lessThan(headerPosition.dx));
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
for (final locale in const [Locale('en'), Locale('zh')]) {
|
||||
for (final width in [320.0, 768.0, 1024.0]) {
|
||||
testWidgets('schedule hides missing update time at width $width in $locale', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
bool? autoUpdate;
|
||||
await tester.pumpWidget(
|
||||
ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (context) => Localizations.override(
|
||||
context: context,
|
||||
locale: locale,
|
||||
delegates: const [AppLocalizations.delegate],
|
||||
child: AppComponentThemes(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: TrackerSubscriptionsSchedule(
|
||||
lastUpdated: null,
|
||||
autoUpdate: false,
|
||||
onAutoUpdateChanged: (value) => autoUpdate = value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('tracker-last-updated')), findsNothing);
|
||||
expect(find.text('Never updated'), findsNothing);
|
||||
expect(find.text('尚未更新'), findsNothing);
|
||||
final scheduleRect = tester.getRect(find.byKey(const ValueKey('tracker-subscriptions-schedule')));
|
||||
final autoRect = tester.getRect(find.byKey(const ValueKey('tracker-auto-update-control')));
|
||||
expect(find.byKey(const ValueKey('tracker-update-button')), findsNothing);
|
||||
expect(autoRect.right, closeTo(scheduleRect.right, 1));
|
||||
await tester.tap(find.byType(Switch));
|
||||
expect(autoUpdate, true);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (final width in [360.0, 768.0, 1024.0]) {
|
||||
testWidgets('long subscription table scrolls within its viewport at width $width', (tester) async {
|
||||
tester.view.physicalSize = Size(width, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
final urls = List.generate(50, (index) => 'https://tracker-$index.example/list.txt');
|
||||
final deletions = <int>[];
|
||||
await tester.pumpWidget(
|
||||
ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: AppComponentThemes(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TrackerSubscriptionsTable(urls: urls, statuses: const {}, onDelete: deletions.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final table = find.byKey(const ValueKey('tracker-subscriptions-table'));
|
||||
final rows = find.byKey(const ValueKey('tracker-subscriptions-rows'));
|
||||
expect(tester.getSize(table).height, lessThan(300));
|
||||
expect(tester.getSize(table).width, width - 32);
|
||||
final headerPosition = tester.getTopLeft(find.text('Subscription URL'));
|
||||
expect(find.byKey(ValueKey('tracker-subscription-${urls.last}')), findsNothing);
|
||||
await tester.dragFrom(tester.getTopLeft(rows) + const Offset(40, 80), const Offset(0, -3000));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(ValueKey('tracker-subscription-${urls.last}')), findsOneWidget);
|
||||
expect(tester.getTopLeft(find.text('Subscription URL')), headerPosition);
|
||||
final delete = find.byKey(ValueKey('delete-settings-list-${urls.last}'));
|
||||
final deleteRect = tester.getRect(delete);
|
||||
final actionsHeader = tester.getTopLeft(find.byKey(const ValueKey('tracker-subscriptions-actions-header')));
|
||||
expect(deleteRect.right, lessThanOrEqualTo(tester.getRect(table).right));
|
||||
expect(deleteRect.left, greaterThan(tester.getRect(table).left));
|
||||
expect(delete.hitTestable(), findsOneWidget);
|
||||
// Data columns scroll, while the delete column remains visible and fixed.
|
||||
if (width < 672) {
|
||||
final headerPosition = tester.getTopLeft(find.text('Subscription URL'));
|
||||
await tester.dragFrom(tester.getTopLeft(table) + const Offset(100, 20), const Offset(-30, 0));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.dragFrom(
|
||||
tester.getBottomLeft(table) + const Offset(40, -5),
|
||||
const Offset(600, 0),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getTopLeft(find.text('Subscription URL')).dx, lessThan(headerPosition.dx));
|
||||
}
|
||||
expect(tester.getRect(delete), deleteRect);
|
||||
expect(tester.getTopLeft(find.byKey(const ValueKey('tracker-subscriptions-actions-header'))), actionsHeader);
|
||||
expect(find.byIcon(Icons.edit_outlined), findsNothing);
|
||||
if (width < 672) {
|
||||
final rightInset = tester.getRect(table).right - deleteRect.right;
|
||||
tester.view.physicalSize = const Size(768, 900);
|
||||
await tester.pumpAndSettle();
|
||||
expect(delete.hitTestable(), findsOneWidget);
|
||||
expect(tester.getRect(table).right - tester.getRect(delete).right, closeTo(rightInset, 1));
|
||||
}
|
||||
await tester.tap(delete);
|
||||
expect(deletions, [49]);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user