refactor: remove proxy API (#1596)

This commit is contained in:
Levi authored and GitHub committed 2026-10-21 22:48:17 +08:00
1 parent 47cafb7575
commit 4214765d39
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 {
@@ -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);
});
}
}