Merge branch 'main' into codex/fix-settings-script-path-picker

This commit is contained in:
Levi authored and GitHub committed 2026-09-09 23:41:17 +08:00
commit f0aa44455c
27 files changed
+1210 -187

No files matched your search

@@ -1,5 +1,13 @@
import 'dart:async';
import 'package:app_links_platform_interface/app_links_platform_interface.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/api/model/downloader_config.dart';
import 'package:gopeed/app/application/app_runtime_controller.dart';
import 'package:gopeed/app/application/app_deep_link_controller.dart';
import 'package:gopeed/core/common/api_server_state.dart';
import 'package:gopeed/core/common/start_config.dart';
import 'package:share_handler/share_handler.dart';
void main() {
@@ -27,4 +35,71 @@ void main() {
expect(sharedMediaUri(media), Uri.parse('magnet:?xt=urn:btih:example'));
});
test('keeps listening for browser takeover links after runtime settings change', () async {
final originalPlatform = AppLinksPlatform.instance;
final appLinksPlatform = _FakeAppLinksPlatform();
AppLinksPlatform.instance = appLinksPlatform;
final container = ProviderContainer(
overrides: [appRuntimeControllerProvider.overrideWith(_FakeRuntimeController.new)],
);
try {
await container.read(appDeepLinkControllerProvider.future);
expect(appLinksPlatform.hasListener, isTrue);
container.read(appRuntimeControllerProvider.notifier).replaceDownloaderConfig(DownloaderConfig());
await Future<void>.delayed(Duration.zero);
await Future<void>.delayed(Duration.zero);
expect(appLinksPlatform.hasListener, isTrue);
} finally {
container.dispose();
await Future<void>.delayed(Duration.zero);
AppLinksPlatform.instance = originalPlatform;
await appLinksPlatform.close();
}
});
}
class _FakeAppLinksPlatform extends AppLinksPlatform {
final _links = StreamController<Uri>.broadcast();
bool get hasListener => _links.hasListener;
@override
Future<Uri?> getInitialLink() async => null;
@override
Stream<Uri> get uriLinkStream => _links.stream;
Future<void> close() => _links.close();
}
class _FakeRuntimeController extends AppRuntimeController {
@override
Future<AppRuntimeState> build() async {
final config = StartConfig()
..network = 'tcp'
..address = '127.0.0.1:9999'
..apiEnable = true
..apiToken = ''
..storage = 'bolt'
..storageDir = ''
..refreshInterval = 0;
return AppRuntimeState(
startConfig: config,
apiServerState: const ApiServerState(
enabled: true,
mcpEnabled: false,
running: true,
network: 'tcp',
address: '127.0.0.1:9999',
runningPort: 9999,
pendingApply: false,
lastError: '',
),
downloaderConfig: DownloaderConfig(),
);
}
}
@@ -0,0 +1,29 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/core/common/libgopeed_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('gopeed.com/libgopeed');
final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
tearDown(() {
messenger.setMockMethodCallHandler(channel, null);
});
test('mobile invokes carry unique request IDs for async callbacks', () async {
final requestIDs = <int>[];
messenger.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'invoke');
final arguments = (call.arguments as Map<Object?, Object?>).cast<String, Object?>();
requestIDs.add(arguments['requestID']! as int);
return '{"code":0}';
});
final bridge = LibgopeedChannel();
await Future.wait([bridge.invoke('GET', '/api/v1/info'), bridge.invoke('GET', '/api/v1/config')]);
expect(requestIDs, [0, 1]);
});
}
@@ -14,6 +14,7 @@ import 'package:gopeed/core/capabilities/capability_rpc.dart';
import 'package:gopeed/core/capabilities/gopeed_capability.dart';
import 'package:gopeed/core/capabilities/storage_capability.dart';
import 'package:gopeed/features/tasks/presentation/pages/create_task_window_page.dart';
import 'package:gopeed/features/tasks/application/pending_create_task.dart';
import 'package:gopeed/shared/services/download_directory_picker.dart';
import 'package:gopeed/shared/theme/app_component_themes.dart';
import 'package:gopeed/shared/theme/app_design_tokens.dart';
@@ -22,6 +23,90 @@ import 'package:gopeed/shared/widgets/app_tooltip.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
void main() {
testWidgets('repeated external tasks update the open create route and replace request metadata', (tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
try {
tester.view.physicalSize = const Size(700, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
CreateTask? submitted;
final registry = CapabilityRegistry(createAppCapabilityCodecs())
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig(downloadDir: '/downloads'))
..bind(GopeedMethods.createTask, (task) {
submitted = task;
return 'created-id';
})
..bind(StorageMethods.saveCreateHistory, (_) => const RpcUnit());
final container = ProviderContainer(
overrides: [appCapabilitiesProvider.overrideWithValue(AppCapabilities(LocalCapabilityInvoker(registry)))],
);
addTearDown(container.dispose);
final pending = container.read(pendingCreateTaskProvider.notifier);
pending.set(
CreateTask(
req: Request(
url: 'https://example.com/first.zip',
rawUrl: 'https://example.com/first',
labels: {'source': 'first'},
extra: ReqExtraHttp(method: 'POST', body: 'old-body', header: {'Cookie': 'old-cookie'}).toJson(),
proxy: RequestProxy(mode: RequestProxyMode.custom, host: 'localhost:8080'),
skipVerifyCert: true,
),
),
);
final router = GoRouter(
initialLocation: '/create',
routes: [
GoRoute(path: '/', builder: (_, _) => const SizedBox()),
GoRoute(path: '/create', builder: (_, _) => const CreateTaskWindowPage()),
],
);
addTearDown(router.dispose);
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: shad.ShadcnApp.router(
theme: AppTheme.light(),
materialTheme: AppTheme.materialLight(),
routerConfig: router,
builder: (_, child) => AppComponentThemes(child: child!),
),
),
);
await tester.pumpAndSettle();
expect(_fieldText(tester, 'create-task-url-input'), 'https://example.com/first.zip');
expect(container.read(pendingCreateTaskProvider), isNull);
final pageState = tester.state(find.byType(CreateTaskWindowPage));
for (final url in [
'https://example.com/second.zip',
'https://example.com/third.zip',
'https://example.com/third.zip',
]) {
pending.set(CreateTask(req: Request(url: url)));
router.go('/create');
await tester.pumpAndSettle();
expect(tester.state(find.byType(CreateTaskWindowPage)), same(pageState));
expect(_fieldText(tester, 'create-task-url-input'), url);
expect(container.read(pendingCreateTaskProvider), isNull);
}
await tester.tap(find.byKey(const ValueKey('create-task-direct-download-toggle')));
await tester.tap(find.text('Confirm'));
await tester.pumpAndSettle();
expect(submitted?.req?.url, 'https://example.com/third.zip');
expect(submitted?.req?.rawUrl, isNull);
expect(submitted?.req?.labels, isNull);
expect(submitted?.req?.extra, isNull);
expect(submitted?.req?.proxy?.mode, RequestProxyMode.follow);
expect(submitted?.req?.skipVerifyCert, isFalse);
expect(tester.takeException(), isNull);
} finally {
debugDefaultTargetPlatformOverride = null;
}
});
testWidgets('mobile create task page starts below the system status bar', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
tester.view.physicalSize = const Size(700, 760);