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

+12
View File
@@ -109,6 +109,18 @@ Guidelines:
- forbidden Material product-component usage
- If a screen already uses `shadcn_flutter`, do not mix in Material product widgets for convenience. Replace them with `shadcn_flutter` equivalents or shared wrappers built on top of `shadcn_flutter`.
### 3.3.1 Text Input Rules
- All product-facing editable text controls must use the shared `AppTextField` from `lib/shared/widgets/app_text_field.dart`.
- This rule applies to every input variant, including search fields, login and password fields, URL fields, numeric fields, multiline editors, settings controls, dialog inputs, and feature-specific form fields.
- Do not instantiate Flutter Material `TextField` or `shadcn_flutter`'s `TextField` directly in feature or page code. Direct construction is allowed only while implementing or extending a shared input primitive, with the reason documented in that primitive.
- `AppTextField` owns the cross-platform text-selection menu policy. Android, iOS, desktop, and Web must all use Flutter's adaptive native context menu so long-press and right-click selection actions follow platform behavior without `shadcn_flutter`'s full-screen context-menu overlay.
- Do not use `shadcn_flutter`'s editable-text context menu on any platform.
- Feature and page code must not override `contextMenuBuilder`. Any exception must be implemented centrally in `AppTextField`, justified by a platform requirement, and covered by Android/iOS and desktop tests.
- Preserve existing field behavior when migrating to `AppTextField`, including controllers, focus nodes, input formatters, autofill hints, password visibility, read-only state, input features, keyboard actions, and multiline sizing.
- Prefer stable keys or `AppTextField` when locating app inputs in widget tests. Do not couple product tests to the exact runtime type of the underlying `shadcn_flutter` implementation.
- Changes to the shared input policy must include regression coverage for Android long-press behavior and desktop right-click behavior. Web builds must keep the same shared native-menu policy.
## 3.4) Desktop Design Metrics
Use the following desktop reference metrics unless a specific screen requires a documented exception:
+1
View File
@@ -78,6 +78,7 @@ class _GopeedAppState extends ConsumerState<GopeedApp> with WidgetsBindingObserv
return shad.ShadcnApp.router(
debugShowCheckedModeBanner: false,
disableBrowserContextMenu: false,
onGenerateTitle: (context) => context.l10n.appTitle,
theme: AppTheme.light(themeColor),
darkTheme: AppTheme.dark(themeColor),
@@ -6,6 +6,7 @@ import '../../../../l10n/l10n.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
class LoginForm extends StatelessWidget {
const LoginForm({
@@ -159,7 +160,7 @@ class _LoginField extends StatelessWidget {
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w600),
),
const SizedBox(height: AppDesignTokens.space8),
shad.TextField(
AppTextField(
key: fieldKey,
controller: controller,
focusNode: focusNode,
@@ -15,6 +15,7 @@ import '../../../../core/window/app_window_chrome.dart';
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/app_toast.dart';
import '../../../../l10n/l10n.dart';
@@ -629,7 +630,7 @@ class _Toolbar extends StatelessWidget {
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
final manualInstallBusy = state.busyExtensionIds.contains(ExtensionsController.manualInstallBusyKey);
final search = shad.TextField(
final search = AppTextField(
key: const ValueKey('extension-search-input'),
controller: searchController,
placeholder: Text(context.l10n.searchExtensions, style: TextStyle(color: palette.searchHint)),
@@ -826,7 +827,7 @@ class _InstallPopoverState extends State<_InstallPopover> {
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
shad.TextField(
AppTextField(
key: const ValueKey('extension-install-url-input'),
controller: widget.controller,
autofocus: true,
@@ -1222,9 +1223,9 @@ class _ExtensionSettingField extends StatelessWidget {
],
const SizedBox(height: 8),
if (setting.type == api_extension.SettingType.boolean)
shad.TextField(controller: controller, placeholder: Text(context.l10n.booleanValueHint))
AppTextField(controller: controller, placeholder: Text(context.l10n.booleanValueHint))
else
shad.TextField(
AppTextField(
controller: controller,
keyboardType: setting.type == api_extension.SettingType.number ? TextInputType.number : TextInputType.text,
),
@@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../l10n/l10n.dart';
class TasksTopBar extends StatelessWidget {
@@ -110,7 +111,7 @@ class _SearchField extends StatelessWidget {
),
child: SizedBox(
height: 40,
child: shad.TextField(
child: AppTextField(
controller: controller,
style: TextStyle(color: palette.textPrimary, fontSize: 14, height: 1),
placeholder: Text(
@@ -24,6 +24,7 @@ import '../../../../shared/widgets/app_choice_segmented_control.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import '../../../../shared/widgets/app_number_input.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/app_toast.dart';
import '../../../../shared/widgets/responsive_menu_layout.dart';
@@ -1849,7 +1850,7 @@ class _TextSettingControl extends StatelessWidget {
final desktop = MediaQuery.sizeOf(context).width >= Breakpoints.mobile;
return SizedBox(
width: desktop ? AppDesignTokens.settingsFormControlWidth : double.infinity,
child: shad.TextField(
child: AppTextField(
key: fieldKey,
controller: controller,
hintText: hintText,
@@ -1919,7 +1920,7 @@ class _HostPortControl extends StatelessWidget {
child: Row(
children: [
Expanded(
child: shad.TextField(
child: AppTextField(
key: hostKey,
controller: hostController,
hintText: context.l10n.server,
@@ -8,6 +8,7 @@ import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../l10n/l10n.dart';
class DownloadCategoryDraft {
@@ -169,7 +170,7 @@ Future<DownloadCategoryDraft?> showDownloadCategoryDialog(
children: [
Text(dialogContext.l10n.categoryName, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
shad.TextField(key: const ValueKey('download-category-name'), controller: nameController),
AppTextField(key: const ValueKey('download-category-name'), controller: nameController),
const SizedBox(height: 14),
Text(dialogContext.l10n.categoryPath, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
@@ -9,6 +9,7 @@ import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_loading_button.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../l10n/l10n.dart';
class SettingsListEntry {
@@ -189,7 +190,7 @@ Future<String?> showTextSettingDialog(
Text(fieldLabel, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
if (pickPath == null)
shad.TextField(controller: controller)
AppTextField(controller: controller)
else
AppPathPickerField(
controller: controller,
@@ -302,7 +303,7 @@ Future<GithubMirrorDraft?> showGithubMirrorDialog(BuildContext context, {GithubM
const SizedBox(height: 14),
Text(dialogContext.l10n.githubMirrorUrl, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
const SizedBox(height: 6),
shad.TextField(controller: controller),
AppTextField(controller: controller),
if (validationMessage != null) ...[
const SizedBox(height: 8),
Text(validationMessage!, style: TextStyle(color: palette.error, fontSize: 12)),
@@ -32,6 +32,7 @@ import '../../../../shared/widgets/app_http_headers_editor.dart';
import '../../../../shared/widgets/app_number_input.dart';
import '../../../../shared/widgets/app_path_picker_field.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/app_toast.dart';
import '../../../../l10n/l10n.dart';
@@ -257,7 +258,12 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
final page = Scaffold(
backgroundColor: palette.sideBg,
child: Padding(
padding: EdgeInsets.only(top: AppWindowChrome.reservesHeaderInset ? AppDesignTokens.windowHeaderHeight : 0),
key: const ValueKey('create-task-safe-content'),
padding: EdgeInsets.only(
top: AppWindowChrome.reservesHeaderInset
? AppDesignTokens.windowHeaderHeight
: MediaQuery.paddingOf(context).top,
),
child: Column(
children: [
Container(
@@ -300,7 +306,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
children: [
SizedBox(
height: 120,
child: TextField(
child: AppTextField(
controller: _urlController,
hintText: context.l10n.pasteDownloadLinks,
keyboardType: TextInputType.multiline,
@@ -645,7 +651,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
label: context.l10n.trackers,
child: SizedBox(
height: 96,
child: TextField(
child: AppTextField(
controller: _trackersController,
hintText: context.l10n.oneTrackerPerLine,
keyboardType: TextInputType.multiline,
@@ -1233,7 +1239,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
children: [
SizedBox(
height: 38,
child: TextField(
child: AppTextField(
key: const ValueKey('create-history-filter'),
controller: filterController,
placeholder: Text(
@@ -1660,7 +1666,7 @@ class _WindowTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return TextField(
return AppTextField(
controller: controller,
hintText: hintText,
keyboardType: keyboardType,
@@ -9,6 +9,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../../../../shared/theme/app_design_tokens.dart';
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_copy_icon_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/app_tooltip.dart';
import '../../../../shared/widgets/detail/app_detail_surface.dart';
import '../../../../core/utils/text_wrap.dart';
@@ -512,7 +513,7 @@ class _EditableUrlBlock extends StatelessWidget {
children: [
SizedBox(
height: 76,
child: shad.TextField(
child: AppTextField(
controller: controller,
minLines: null,
maxLines: null,
@@ -4,6 +4,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' hide Column, Expanded, Row;
import '../../../../shared/theme/app_palette.dart';
import '../../../../shared/widgets/app_http_headers_editor.dart';
import '../../../../shared/widgets/app_primary_button.dart';
import '../../../../shared/widgets/app_text_field.dart';
import '../../../../l10n/l10n.dart';
import '../../domain/task_record.dart';
@@ -47,7 +48,7 @@ Future<TaskUrlUpdate?> showTaskUpdateUrlDialog(BuildContext context, TaskRecord
children: [
Text(dialogContext.l10n.downloadLink, style: TextStyle(color: palette.textPrimary, fontSize: 12)),
const SizedBox(height: 6),
TextField(controller: urlController),
AppTextField(controller: urlController),
const SizedBox(height: 18),
AppHttpHeadersEditor(
controller: headersController,
@@ -5,6 +5,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../theme/app_design_tokens.dart';
import '../theme/app_palette.dart';
import '../../l10n/l10n.dart';
import 'app_text_field.dart';
class AppHttpHeadersController extends ChangeNotifier {
AppHttpHeadersController({Map<String, String>? headers, Iterable<String> defaultNames = const []}) {
@@ -171,7 +172,7 @@ class _HeaderTextField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final palette = AppPalette.of(context);
return shad.TextField(
return AppTextField(
controller: controller,
hintText: hintText,
filled: true,
@@ -38,7 +38,7 @@ class AppLoadingButton extends StatelessWidget {
children: [
leading,
const SizedBox(width: AppDesignTokens.space8),
effectiveChild,
Flexible(child: effectiveChild),
],
);
final effectiveOnPressed = loading ? null : onPressed;
@@ -2,6 +2,8 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import 'app_text_field.dart';
/// A bounded numeric text field with the app's standard spinner controls.
class AppNumberInput extends StatefulWidget {
const AppNumberInput({
@@ -80,7 +82,7 @@ class _AppNumberInputState extends State<AppNumberInput> {
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: widget.min.toInt(), max: widget.max?.toInt()),
];
return shad.TextField(
return AppTextField(
key: widget.fieldKey,
controller: widget.controller,
hintText: widget.hintText,
@@ -4,6 +4,7 @@ import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
import '../../core/utils/breakpoints.dart';
import '../services/download_directory_picker.dart';
import 'app_text_field.dart';
import 'app_tooltip.dart';
enum AppPathPickerButtonStyle { ghost, outline }
@@ -119,7 +120,7 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
onEnter: (_) => _syncTooltipPath(),
child: AppTooltip(
message: _tooltipPath,
child: shad.TextField(
child: AppTextField(
key: widget.fieldKey,
controller: widget.controller,
hintText: widget.hintText,
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart' as material show AdaptiveTextSelectionToolbar;
import 'package:flutter/widgets.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
/// The app-wide text field.
///
/// shadcn_flutter's custom context menu is rendered in a full-screen overlay
/// that can cover the app with an opaque surface. Use Flutter's adaptive
/// platform toolbar everywhere so long-press and right-click text actions have
/// consistent native behavior on mobile, desktop, and web.
class AppTextField extends shad.TextField {
const AppTextField({
super.key,
super.groupId,
super.controller,
super.initialValue,
super.focusNode,
super.undoController,
super.decoration,
super.padding,
super.placeholder,
super.crossAxisAlignment,
super.clearButtonSemanticLabel,
super.keyboardType,
super.textInputAction,
super.textCapitalization,
super.style,
super.strutStyle,
super.textAlign,
super.textAlignVertical,
super.textDirection,
super.readOnly,
super.showCursor,
super.autofocus,
super.obscuringCharacter,
super.obscureText,
super.autocorrect,
super.smartDashesType,
super.smartQuotesType,
super.enableSuggestions,
super.maxLines,
super.minLines,
super.expands,
super.maxLength,
super.maxLengthEnforcement,
super.onChanged,
super.onEditingComplete,
super.onSubmitted,
super.onTapOutside,
super.onTapUpOutside,
super.inputFormatters,
super.enabled,
super.cursorWidth,
super.cursorHeight,
super.cursorRadius,
super.cursorOpacityAnimates,
super.cursorColor,
super.selectionHeightStyle,
super.selectionWidthStyle,
super.keyboardAppearance,
super.scrollPadding,
super.enableInteractiveSelection,
super.selectionControls,
super.dragStartBehavior,
super.scrollController,
super.scrollPhysics,
super.onTap,
super.autofillHints,
super.clipBehavior,
super.restorationId,
super.stylusHandwritingEnabled,
super.enableIMEPersonalizedLearning,
super.contentInsertionConfiguration,
super.contextMenuBuilder = appTextFieldContextMenuBuilder,
super.hintText,
super.border,
super.borderRadius,
super.filled,
super.statesController,
super.magnifierConfiguration,
super.spellCheckConfiguration,
super.features,
super.submitFormatters,
super.skipInputFeatureFocusTraversal,
super.onDragSelectionStart,
super.onDragSelectionUpdate,
super.onDragSelectionEnd,
});
}
Widget appTextFieldContextMenuBuilder(BuildContext context, EditableTextState editableTextState) {
// Match Flutter's platform text-menu policy without falling back to
// shadcn_flutter's full-screen overlay. EditableText bypasses this builder
// on Web while the browser context menu is enabled.
if (SystemContextMenu.isSupportedByField(editableTextState)) {
return SystemContextMenu.editableText(editableTextState: editableTextState);
}
return material.AdaptiveTextSelectionToolbar.editableText(editableTextState: editableTextState);
}
@@ -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));