mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-08 09:30:27 +00:00
Merge branch 'main' into codex/async-ffi-worker-callbacks
This commit is contained in:
21 files changed
+604
-193
No files matched your search
@@ -0,0 +1,88 @@
|
||||
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';
|
||||
import 'package:gopeed/app/application/app_platform_controller.dart';
|
||||
import 'package:gopeed/app/application/app_runtime_controller.dart';
|
||||
import 'package:gopeed/core/common/api_server_state.dart';
|
||||
import 'package:gopeed/core/common/start_config.dart';
|
||||
import 'package:gopeed/util/updater.dart';
|
||||
|
||||
void main() {
|
||||
test('runtime updates do not rebuild the platform controller', () async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
appRuntimeControllerProvider.overrideWith(_TestRuntimeController.new),
|
||||
appPlatformControllerProvider.overrideWith(_TestPlatformController.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
final subscription = container.listen(appPlatformControllerProvider, (_, _) {}, fireImmediately: true);
|
||||
addTearDown(subscription.close);
|
||||
|
||||
await container.read(appPlatformControllerProvider.future);
|
||||
final platformController = container.read(appPlatformControllerProvider.notifier) as _TestPlatformController;
|
||||
final runtimeController = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController;
|
||||
|
||||
expect(platformController.buildCount, 1);
|
||||
|
||||
runtimeController.completeRuntime();
|
||||
await container.read(appRuntimeControllerProvider.future);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(platformController.updateCheckCount, 1);
|
||||
|
||||
final config = DownloaderConfig.fromJson(runtimeController.state.requireValue.downloaderConfig.toJson());
|
||||
config.extra.locale = 'zh';
|
||||
runtimeController.replaceDownloaderConfig(config);
|
||||
await container.pump();
|
||||
|
||||
expect(platformController.buildCount, 1);
|
||||
});
|
||||
}
|
||||
|
||||
class _TestPlatformController extends AppPlatformController {
|
||||
int buildCount = 0;
|
||||
int updateCheckCount = 0;
|
||||
|
||||
@override
|
||||
bool get isDesktop => false;
|
||||
|
||||
@override
|
||||
Future<AppPlatformState> build() {
|
||||
buildCount++;
|
||||
return super.build();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<VersionInfo?> checkForUpdate() async {
|
||||
updateCheckCount++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _TestRuntimeController extends AppRuntimeController {
|
||||
final _pendingRuntime = Completer<AppRuntimeState>();
|
||||
|
||||
@override
|
||||
Future<AppRuntimeState> build() => _pendingRuntime.future;
|
||||
|
||||
void completeRuntime() {
|
||||
_pendingRuntime.complete(
|
||||
AppRuntimeState(
|
||||
startConfig: StartConfig(),
|
||||
apiServerState: const ApiServerState(
|
||||
enabled: false,
|
||||
mcpEnabled: false,
|
||||
running: false,
|
||||
network: '',
|
||||
address: '',
|
||||
runningPort: 0,
|
||||
pendingApply: false,
|
||||
lastError: '',
|
||||
),
|
||||
downloaderConfig: DownloaderConfig(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/widgets.dart' as flutter show Row;
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -21,6 +22,39 @@ import 'package:gopeed/shared/widgets/app_tooltip.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
testWidgets('mobile create task page starts below the system status bar', (WidgetTester tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
tester.view.physicalSize = const Size(700, 760);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.padding = const FakeViewPadding(top: 32, bottom: 24);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig(downloadDir: '/downloads'));
|
||||
final capabilities = AppCapabilities(LocalCapabilityInvoker(registry));
|
||||
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [appCapabilitiesProvider.overrideWithValue(capabilities)],
|
||||
child: shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(child: const CreateTaskWindowPage()),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final contentPadding = tester.widget<Padding>(find.byKey(const ValueKey('create-task-safe-content')));
|
||||
expect((contentPadding.padding as EdgeInsets).top, 32);
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('browser extension request parameters populate the form and survive submission', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@@ -119,6 +153,19 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final connectionsInput = find.byKey(const ValueKey('create-task-connections-input'));
|
||||
expect(connectionsInput, findsOneWidget);
|
||||
expect(tester.getSize(connectionsInput).width, AppDesignTokens.settingsNumberControlWidth);
|
||||
final connectionsField = tester.widget<shad.TextField>(connectionsInput);
|
||||
expect(connectionsField.controller!.text, '12');
|
||||
expect(connectionsField.features.single, isA<shad.InputSpinnerFeature>());
|
||||
|
||||
final advancedSection = find.byKey(const ValueKey('create-task-advanced-section'));
|
||||
expect(tester.widget<AnimatedCrossFade>(advancedSection).crossFadeState, CrossFadeState.showSecond);
|
||||
await tester.tap(find.text('Advanced'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<AnimatedCrossFade>(advancedSection).crossFadeState, CrossFadeState.showFirst);
|
||||
|
||||
expect(_fieldText(tester, 'create-task-http-header-name-0'), 'User-Agent');
|
||||
expect(_fieldText(tester, 'create-task-http-header-value-0'), 'Browser UA');
|
||||
expect(_fieldText(tester, 'create-task-http-header-name-3'), 'Authorization');
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' as material show AdaptiveTextSelectionToolbar;
|
||||
import 'package:flutter/services.dart' show BrowserContextMenu;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||
import 'package:gopeed/shared/widgets/app_text_field.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||
|
||||
void main() {
|
||||
testWidgets('Android long press uses Flutter adaptive text selection toolbar', (tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
final controller = TextEditingController(text: 'https://example.com/download');
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(
|
||||
child: Center(
|
||||
child: SizedBox(width: 320, child: AppTextField(controller: controller)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.longPress(find.byType(AppTextField));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(material.AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||
expect(find.byType(shad.MobileEditableTextContextMenu), findsNothing);
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('desktop right click uses Flutter adaptive text selection toolbar', (tester) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||
final controller = TextEditingController(text: 'https://example.com/download');
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(
|
||||
child: Center(
|
||||
child: SizedBox(width: 320, child: AppTextField(controller: controller)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final field = find.byType(AppTextField);
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(field),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(material.AdaptiveTextSelectionToolbar), findsOneWidget);
|
||||
expect(find.byType(shad.DesktopEditableTextContextMenu), findsNothing);
|
||||
} finally {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
}
|
||||
}, skip: kIsWeb);
|
||||
|
||||
testWidgets('Web right click keeps the browser menu and creates no Flutter overlay', (tester) async {
|
||||
final controller = TextEditingController(text: 'https://example.com/download');
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
shad.ShadcnApp(
|
||||
disableBrowserContextMenu: false,
|
||||
theme: AppTheme.light(),
|
||||
materialTheme: AppTheme.materialLight(),
|
||||
home: AppComponentThemes(
|
||||
child: Center(
|
||||
child: SizedBox(width: 320, child: AppTextField(controller: controller)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(BrowserContextMenu.enabled, isTrue);
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.byType(AppTextField)),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(material.AdaptiveTextSelectionToolbar), findsNothing);
|
||||
expect(find.byType(shad.DesktopEditableTextContextMenu), findsNothing);
|
||||
expect(find.byType(shad.MobileEditableTextContextMenu), findsNothing);
|
||||
}, skip: !kIsWeb);
|
||||
|
||||
test('AppTextField defaults to the shared context menu policy', () {
|
||||
expect(const AppTextField().contextMenuBuilder, same(appTextFieldContextMenuBuilder));
|
||||
});
|
||||
}
|
||||
@@ -81,6 +81,7 @@ import 'package:gopeed/shared/widgets/app_copy_icon_button.dart';
|
||||
import 'package:gopeed/shared/widgets/app_http_headers_editor.dart';
|
||||
import 'package:gopeed/shared/widgets/app_path_picker_field.dart';
|
||||
import 'package:gopeed/shared/widgets/app_primary_button.dart';
|
||||
import 'package:gopeed/shared/widgets/app_text_field.dart';
|
||||
import 'package:gopeed/shared/widgets/app_tooltip.dart';
|
||||
import 'package:gopeed/shared/widgets/app_toast.dart';
|
||||
import 'package:gopeed/shared/widgets/gopeed_app_mark.dart';
|
||||
@@ -371,7 +372,7 @@ void main() {
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.enterText(find.byType(shad.TextField), 'not-a-task');
|
||||
await tester.enterText(find.byType(AppTextField), 'not-a-task');
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('No matching tasks found'), findsOneWidget);
|
||||
@@ -1451,8 +1452,10 @@ void main() {
|
||||
await tester.tap(find.byKey(const ValueKey('close-mcp-agent-setup')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.ensureVisible(find.byKey(const ValueKey('mcp-endpoint-switch')));
|
||||
await tester.tap(find.byKey(const ValueKey('mcp-endpoint-switch')));
|
||||
final mcpSwitch = find.byKey(const ValueKey('mcp-endpoint-switch'));
|
||||
await Scrollable.ensureVisible(tester.element(mcpSwitch), alignment: 0.5);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(mcpSwitch);
|
||||
await tester.pump();
|
||||
expect(tester.widget<AppLoadingButton>(saveButton).onPressed, isNotNull);
|
||||
|
||||
@@ -2302,7 +2305,7 @@ void main() {
|
||||
expect(tester.getSize(createDirectoryInput).height, tester.getSize(createRenameInput).height);
|
||||
final createDirectoryField = tester.widget<shad.TextField>(createDirectoryInput);
|
||||
final createRenameField = tester.widget<shad.TextField>(
|
||||
find.descendant(of: createRenameInput, matching: find.byType(shad.TextField)),
|
||||
find.descendant(of: createRenameInput, matching: find.byType(AppTextField)),
|
||||
);
|
||||
expect(createDirectoryField.filled, createRenameField.filled);
|
||||
expect(createDirectoryField.border, createRenameField.border);
|
||||
@@ -4339,8 +4342,8 @@ void main() {
|
||||
expect(find.byType(AppHttpHeadersEditor), findsOneWidget);
|
||||
final firstName = find.byKey(const ValueKey('update-task-http-header-name-0'));
|
||||
final firstValue = find.byKey(const ValueKey('update-task-http-header-value-0'));
|
||||
final firstNameField = find.descendant(of: firstName, matching: find.byType(shad.TextField));
|
||||
final firstValueField = find.descendant(of: firstValue, matching: find.byType(shad.TextField));
|
||||
final firstNameField = find.descendant(of: firstName, matching: find.byType(AppTextField));
|
||||
final firstValueField = find.descendant(of: firstValue, matching: find.byType(AppTextField));
|
||||
expect(tester.widget<shad.TextField>(firstNameField).controller!.text, 'Authorization');
|
||||
expect(tester.widget<shad.TextField>(firstValueField).controller!.text, 'Bearer old');
|
||||
expect(tester.getSize(firstValue).width / tester.getSize(firstName).width, closeTo(1.618, 0.01));
|
||||
|
||||
Reference in new issue
Block a user