fix(ui): improve cross-platform text input behavior (#1477)

This commit is contained in:
Levi authored and GitHub committed 2026-09-06 17:18:57 +08:00
1 parent 16f2d30476
commit d511e66d56
19 files changed
+305 -28

No files matched your search

@@ -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 {
@@ -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));
});
}
+9 -6
View File
@@ -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));