diff --git a/bin/internal/engine.version b/bin/internal/engine.version index b05e06debbdb..a5e493026292 100644 --- a/bin/internal/engine.version +++ b/bin/internal/engine.version @@ -1 +1 @@ -d492b8b3a56729397743957de82be7cc1c6f4c6d +d1dad6acbe5c5fcfa885e9bf4f3e1f091020ccb8 diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 49da27a3034b..8fc9190d32ee 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -f224eea858f23b4d23b1a4f2f556b3f35c134b01 +3f480616948dda17f4324985057f154f56371e1e diff --git a/bin/internal/fuchsia-linux.version b/bin/internal/fuchsia-linux.version index 9d0899e31b12..52c948b76b19 100644 --- a/bin/internal/fuchsia-linux.version +++ b/bin/internal/fuchsia-linux.version @@ -1 +1 @@ -rQKj0gZPJNATiErM86LM5BALaHqREfrozRBvIuYO9u0C +nS_R3f779FLIEjE8Isvhlp0nihexhl8MrL3sJtIK7WUC diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 0e64a032d13c..3811adf5043b 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -534,6 +534,14 @@ Future verifyDeprecations(String workingDirectory, { int minimumMatches = String possibleReason = ''; if (lines[lineNumber].trimLeft().startsWith('"')) { possibleReason = ' You might have used double quotes (") for the string instead of single quotes (\').'; + } else if (!lines[lineNumber].contains("'")) { + possibleReason = ' It might be missing the line saying "This feature was deprecated after...".'; + } else if (!lines[lineNumber].trimRight().endsWith(" '")) { + if (lines[lineNumber].contains('This feature was deprecated')) { + possibleReason = ' There might not be an explanatory message.'; + } else { + possibleReason = ' There might be a missing space character at the end of the line.'; + } } throw 'Deprecation notice does not match required pattern.$possibleReason'; } @@ -546,6 +554,8 @@ Future verifyDeprecations(String workingDirectory, { int minimumMatches = if (firstChar.toUpperCase() != firstChar) { throw 'Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo'; } + } else { + message += messageMatch.namedGroup('message')!; } lineNumber += 1; if (lineNumber >= lines.length) { @@ -565,7 +575,7 @@ Future verifyDeprecations(String workingDirectory, { int minimumMatches = } } if (!message.endsWith('.') && !message.endsWith('!') && !message.endsWith('?')) { - throw 'Deprecation notice should be a grammatically correct sentence and end with a period.'; + throw 'Deprecation notice should be a grammatically correct sentence and end with a period; notice appears to be "$message".'; } if (!lines[lineNumber].startsWith("$indent '")) { throw 'Unexpected deprecation notice indent.'; diff --git a/dev/bots/post_process_docs.dart b/dev/bots/post_process_docs.dart index 2bc9c0386549..91bdb7086a5a 100644 --- a/dev/bots/post_process_docs.dart +++ b/dev/bots/post_process_docs.dart @@ -94,13 +94,18 @@ Future runProcessWithValidations( List command, String workingDirectory, { @visibleForTesting ProcessManager processManager = const LocalProcessManager(), + bool verbose = true, }) async { final ProcessResult result = processManager.runSync(command, stdoutEncoding: utf8, workingDirectory: workingDirectory); if (result.exitCode == 0) { - print('Stdout: ${result.stdout}'); + if (verbose) { + print('stdout: ${result.stdout}'); + } } else { - print('StdErr: ${result.stderr}'); + if (verbose) { + print('stderr: ${result.stderr}'); + } throw CommandException(); } } diff --git a/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart b/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart index ee4b5efb534b..26bdba38c7a3 100644 --- a/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart +++ b/dev/bots/test/analyze-test-input/root/packages/foo/deprecation.dart @@ -100,3 +100,8 @@ void test17() { } 'This feature was deprecated after v2.1.0-11.0.pre.' ) void test18() { } + +@Deprecated( // flutter_ignore: deprecation_syntax, https://github.com/flutter/flutter/issues/000000 + 'Missing the version line. ' +) +void test19() { } diff --git a/dev/bots/test/analyze_test.dart b/dev/bots/test/analyze_test.dart index 1eaa13b1e8ba..9edaf41ededb 100644 --- a/dev/bots/test/analyze_test.dart +++ b/dev/bots/test/analyze_test.dart @@ -45,13 +45,13 @@ void main() { test('analyze.dart - verifyDeprecations', () async { final String result = await capture(() => verifyDeprecations(testRootPath, minimumMatches: 2), shouldHaveErrors: true); final String lines = [ - '║ test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern.', + '║ test/analyze-test-input/root/packages/foo/deprecation.dart:12: Deprecation notice does not match required pattern. There might be a missing space character at the end of the line.', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:18: Deprecation notice should be a grammatically correct sentence and start with a capital letter; see style guide: STYLE_GUIDE_URL', - '║ test/analyze-test-input/root/packages/foo/deprecation.dart:25: Deprecation notice should be a grammatically correct sentence and end with a period.', + '║ test/analyze-test-input/root/packages/foo/deprecation.dart:25: Deprecation notice should be a grammatically correct sentence and end with a period; notice appears to be "Also bad grammar".', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:29: Deprecation notice does not match required pattern.', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:32: Deprecation notice does not match required pattern.', - '║ test/analyze-test-input/root/packages/foo/deprecation.dart:37: Deprecation notice does not match required pattern.', - '║ test/analyze-test-input/root/packages/foo/deprecation.dart:41: Deprecation notice does not match required pattern.', + '║ test/analyze-test-input/root/packages/foo/deprecation.dart:37: Deprecation notice does not match required pattern. It might be missing the line saying "This feature was deprecated after...".', + '║ test/analyze-test-input/root/packages/foo/deprecation.dart:41: Deprecation notice does not match required pattern. There might not be an explanatory message.', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:48: End of deprecation notice does not match required pattern.', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:51: Unexpected deprecation notice indent.', '║ test/analyze-test-input/root/packages/foo/deprecation.dart:70: Deprecation notice does not accurately indicate a beta branch version number; please see RELEASES_URL to find the latest beta build version number.', diff --git a/dev/bots/test/post_process_docs_test.dart b/dev/bots/test/post_process_docs_test.dart index 043d722a2fbf..d73f280fff7c 100644 --- a/dev/bots/test/post_process_docs_test.dart +++ b/dev/bots/test/post_process_docs_test.dart @@ -118,7 +118,7 @@ void main() async { ), ], ); - await runProcessWithValidations(command, '', processManager: processManager); + await runProcessWithValidations(command, '', processManager: processManager, verbose: false); expect(processManager, hasNoRemainingExpectations); }); @@ -133,7 +133,7 @@ void main() async { ], ); try { - await runProcessWithValidations(command, '', processManager: processManager); + await runProcessWithValidations(command, '', processManager: processManager, verbose: false); throw Exception('Exception was not thrown'); } on CommandException catch (e) { expect(e, isA()); diff --git a/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart b/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart index dec74251ec63..2a3839ab117f 100644 --- a/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart +++ b/dev/integration_tests/web_e2e_tests/test_driver/platform_messages_integration.dart @@ -21,7 +21,7 @@ void main() { await tester.pumpAndSettle(); // TODO(nurhan): https://github.com/flutter/flutter/issues/51885 - SystemChannels.textInput.setMockMethodCallHandler(null); + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, null); // Focus on a TextFormField. final Finder finder = find.byKey(const Key('input')); expect(finder, findsOneWidget); @@ -39,7 +39,7 @@ void main() { platformViewsRegistry.getNextPlatformViewId(); // ignore: undefined_prefixed_name, avoid_dynamic_calls ui.platformViewRegistry.registerViewFactory('MyView', (int viewId) { - ++viewInstanceCount; + viewInstanceCount += 1; return html.DivElement(); }); diff --git a/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart b/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart index f3f7764d40e9..9637c830ff83 100644 --- a/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart +++ b/examples/api/test/widgets/binding/widget_binding_observer.0_test.dart @@ -13,8 +13,8 @@ void main() { Future setAppLifeCycleState(AppLifecycleState state) async { final ByteData? message = const StringCodec().encodeMessage(state.toString()); - await ServicesBinding.instance.defaultBinaryMessenger - .handlePlatformMessage('flutter/lifecycle', message, (_) {}); + await tester.binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/lifecycle', message, (_) {}); } await tester.pumpWidget( diff --git a/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart b/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart index f81f3385f0dc..a3b76b30f064 100644 --- a/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart +++ b/examples/api/test/widgets/editable_text/editable_text.on_content_inserted.0_test.dart @@ -44,7 +44,7 @@ void main() { }); try { - await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/textinput', messageBytes, (ByteData? _) {}, diff --git a/packages/flutter/lib/src/material/action_buttons.dart b/packages/flutter/lib/src/material/action_buttons.dart index 6f8eba783711..b57a801ba451 100644 --- a/packages/flutter/lib/src/material/action_buttons.dart +++ b/packages/flutter/lib/src/material/action_buttons.dart @@ -323,7 +323,7 @@ class DrawerButtonIcon extends StatelessWidget { /// A [DrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the /// close button calls [ScaffoldState.openDrawer] to the [Scaffold.drawer]. /// -/// The default behaviour on press can be overriden with [onPressed]. +/// The default behaviour on press can be overridden with [onPressed]. /// /// See also: /// @@ -388,7 +388,7 @@ class EndDrawerButtonIcon extends StatelessWidget { /// A [EndDrawerButton] is an [IconButton] with a "drawer" icon. When pressed, the /// end drawer button calls [ScaffoldState.openEndDrawer] to open the [Scaffold.endDrawer]. /// -/// The default behaviour on press can be overriden with [onPressed]. +/// The default behaviour on press can be overridden with [onPressed]. /// /// See also: /// diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index dcad669a9806..35b655eaa52c 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -321,7 +321,7 @@ class CheckboxListTile extends StatelessWidget { /// If null, then the value of [activeColor] with alpha [kRadialReactionAlpha] /// and [hoverColor] is used in the pressed and hovered state. If that is also null, /// the value of [CheckboxThemeData.overlayColor] is used. If that is also null, - /// then the the default value is used in the pressed and hovered state. + /// then the default value is used in the pressed and hovered state. final MaterialStateProperty? overlayColor; /// {@macro flutter.material.checkbox.splashRadius} diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 350eb9b6c414..f60e3ea3514a 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -127,6 +127,8 @@ class DropdownMenu extends StatefulWidget { this.trailingIcon, this.label, this.hintText, + this.helperText, + this.errorText, this.selectedTrailingIcon, this.enableFilter = false, this.enableSearch = true, @@ -183,6 +185,31 @@ class DropdownMenu extends StatefulWidget { /// Defaults to null; final String? hintText; + /// Text that provides context about the [DropdownMenu]'s value, such + /// as how the value will be used. + /// + /// If non-null, the text is displayed below the input field, in + /// the same location as [errorText]. If a non-null [errorText] value is + /// specified then the helper text is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.helperText], which is the text that provides context about the [InputDecorator.child]'s value. + final String? helperText; + + /// Text that appears below the input field and the border to show the error message. + /// + /// If non-null, the border's color animates to red and the [helperText] is not shown. + /// + /// Defaults to null; + /// + /// See also: + /// + /// * [InputDecoration.errorText], which is the text that appears below the [InputDecorator.child] and the border. + final String? errorText; + /// An optional icon at the end of the text field to indicate that the text /// field is pressed. /// @@ -579,6 +606,8 @@ class _DropdownMenuState extends State> { enabled: widget.enabled, label: widget.label, hintText: widget.hintText, + helperText: widget.helperText, + errorText: widget.errorText, prefixIcon: widget.leadingIcon != null ? Container( key: _leadingKey, child: widget.leadingIcon diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart index c19f1252181b..793f7cad479f 100644 --- a/packages/flutter/lib/src/material/menu_anchor.dart +++ b/packages/flutter/lib/src/material/menu_anchor.dart @@ -3177,7 +3177,12 @@ class _MenuLayout extends SingleChildLayoutDelegate { } else if (offBottom(y)) { final double newY = anchorRect.top - childSize.height; if (!offTop(newY)) { - y = newY; + // Only move the menu up if its parent is horizontal (MenuAchor/MenuBar). + if (parentOrientation == Axis.horizontal) { + y = newY - alignmentOffset.dy; + } else { + y = newY; + } } else { y = allowedRect.bottom - childSize.height; } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index ee673c61ba79..c7c665319a30 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -448,7 +448,7 @@ class ThemeData with Diagnosticable { } pageTransitionsTheme ??= const PageTransitionsTheme(); scrollbarTheme ??= const ScrollbarThemeData(); - visualDensity ??= VisualDensity.adaptivePlatformDensity; + visualDensity ??= VisualDensity.defaultDensityForPlatform(platform); useMaterial3 ??= false; final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 @@ -2646,12 +2646,32 @@ class VisualDensity with Diagnosticable { /// It corresponds to a density value of -2 in both axes. static const VisualDensity compact = VisualDensity(horizontal: -2.0, vertical: -2.0); - /// Returns a visual density that is adaptive based on the [defaultTargetPlatform]. + /// Returns a [VisualDensity] that is adaptive based on the current platform + /// on which the framework is executing, from [defaultTargetPlatform]. /// - /// For desktop platforms, this returns [compact], and for other platforms, - /// it returns a default-constructed [VisualDensity]. - static VisualDensity get adaptivePlatformDensity { - switch (defaultTargetPlatform) { + /// When [defaultTargetPlatform] is a desktop platform, this returns + /// [compact], and for other platforms, it returns a default-constructed + /// [VisualDensity]. + /// + /// See also: + /// + /// * [defaultDensityForPlatform] which returns a [VisualDensity] that is + /// adaptive based on the platform given to it. + /// * [defaultTargetPlatform] which returns the platform on which the + /// framework is currently executing. + static VisualDensity get adaptivePlatformDensity => defaultDensityForPlatform(defaultTargetPlatform); + + /// Returns a [VisualDensity] that is adaptive based on the given [platform]. + /// + /// For desktop platforms, this returns [compact], and for other platforms, it + /// returns a default-constructed [VisualDensity]. + /// + /// See also: + /// + /// * [adaptivePlatformDensity] which returns a [VisualDensity] that is + /// adaptive based on [defaultTargetPlatform]. + static VisualDensity defaultDensityForPlatform(TargetPlatform platform) { + switch (platform) { case TargetPlatform.android: case TargetPlatform.iOS: case TargetPlatform.fuchsia: diff --git a/packages/flutter/lib/src/services/binary_messenger.dart b/packages/flutter/lib/src/services/binary_messenger.dart index b24093c078cf..d7774eae8ea4 100644 --- a/packages/flutter/lib/src/services/binary_messenger.dart +++ b/packages/flutter/lib/src/services/binary_messenger.dart @@ -45,13 +45,12 @@ abstract class BinaryMessenger { /// To register a handler for a given message channel, see [setMessageHandler]. /// /// To send a message _to_ a plugin on the platform thread, see [send]. - // TODO(ianh): deprecate this method once cocoon and other customer_tests are migrated: - // @NotYetDeprecated( - // 'Instead of calling this method, use ServicesBinding.instance.channelBuffers.push. ' - // 'In tests, consider using tester.binding.defaultBinaryMessenger.handlePlatformMessage ' - // 'or TestDefaultBinaryMessenger.instance.defaultBinaryMessenger.handlePlatformMessage. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Instead of calling this method, use ServicesBinding.instance.channelBuffers.push. ' + 'In tests, consider using tester.binding.defaultBinaryMessenger.handlePlatformMessage ' + 'or TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) Future handlePlatformMessage(String channel, ByteData? data, ui.PlatformMessageResponseCallback? callback); /// Send a binary message to the platform plugins on the given channel. diff --git a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart index 2ecbcf0c495b..4b3b7cb6ccc2 100644 --- a/packages/flutter/lib/src/widgets/automatic_keep_alive.dart +++ b/packages/flutter/lib/src/widgets/automatic_keep_alive.dart @@ -144,7 +144,8 @@ class _AutomaticKeepAliveState extends State { } VoidCallback _createCallback(Listenable handle) { - return () { + late final VoidCallback callback; + return callback = () { assert(() { if (!mounted) { throw FlutterError( @@ -157,6 +158,7 @@ class _AutomaticKeepAliveState extends State { return true; }()); _handles!.remove(handle); + handle.removeListener(callback); if (_handles!.isEmpty) { if (SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.persistentCallbacks.index) { // Build/layout haven't started yet so let's just schedule this for diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index b61392b5648d..d7c4bdfe7c70 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -3706,6 +3706,15 @@ class EditableTextState extends State with AutomaticKeepAliveClien } void _didChangeTextEditingValue() { + if (_hasFocus && !_value.selection.isValid) { + // If this field is focused and the selection is invalid, place the cursor at + // the end. Does not rely on _handleFocusChanged because it makes selection + // handles visible on Android. + // Unregister as a listener to the text controller while making the change. + widget.controller.removeListener(_didChangeTextEditingValue); + widget.controller.selection = _adjustedSelectionWhenFocused()!; + widget.controller.addListener(_didChangeTextEditingValue); + } _updateRemoteEditingValueIfNeeded(); _startOrStopCursorTimerIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded(); @@ -3726,21 +3735,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (!widget.readOnly) { _scheduleShowCaretOnScreen(withAnimation: true); } - final bool shouldSelectAll = widget.selectionEnabled && kIsWeb - && !_isMultiline && !_nextFocusChangeIsInternal; - if (shouldSelectAll) { - // On native web, single line tags select all when receiving - // focus. - _handleSelectionChanged( - TextSelection( - baseOffset: 0, - extentOffset: _value.text.length, - ), - null, - ); - } else if (!_value.selection.isValid) { - // Place cursor at the end if the selection is invalid when we receive focus. - _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null); + final TextSelection? updatedSelection = _adjustedSelectionWhenFocused(); + if (updatedSelection != null) { + _handleSelectionChanged(updatedSelection, null); } } else { WidgetsBinding.instance.removeObserver(this); @@ -3749,6 +3746,24 @@ class EditableTextState extends State with AutomaticKeepAliveClien updateKeepAlive(); } + TextSelection? _adjustedSelectionWhenFocused() { + TextSelection? selection; + final bool shouldSelectAll = widget.selectionEnabled && kIsWeb + && !_isMultiline && !_nextFocusChangeIsInternal; + if (shouldSelectAll) { + // On native web, single line tags select all when receiving + // focus. + selection = TextSelection( + baseOffset: 0, + extentOffset: _value.text.length, + ); + } else if (!_value.selection.isValid) { + // Place cursor at the end if the selection is invalid when we receive focus. + selection = TextSelection.collapsed(offset: _value.text.length); + } + return selection; + } + void _compositeCallback(Layer layer) { // The callback can be invoked when the layer is detached. // The input connection can be closed by the platform in which case this diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 526d41cb14d7..a04790e5977f 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -4763,7 +4763,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// cheaper. (Additionally, if _any_ subclass of [Widget] used in an /// application implements `operator ==`, then the compiler cannot inline the /// comparison anywhere, because it has to treat the call as virtual just in - /// case the instance happens to be one that has an overriden operator.) + /// case the instance happens to be one that has an overridden operator.) /// /// Instead, the best way to avoid unnecessary rebuilds is to cache the /// widgets that are returned from [State.build], so that each frame the same diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index a471b47f3db7..f0c05e9e5c99 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -3270,12 +3270,11 @@ enum ClipboardStatus { notPasteable, } +// TODO(justinmc): Deprecate this after TextSelectionControls.buildToolbar is +// deleted, when users should migrate back to TextSelectionControls.buildHandle. +// See https://github.com/flutter/flutter/pull/124262 /// [TextSelectionControls] that specifically do not manage the toolbar in order /// to leave that to [EditableText.contextMenuBuilder]. -@Deprecated( - 'Use `TextSelectionControls`. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', -) mixin TextSelectionHandleControls on TextSelectionControls { @override Widget buildToolbar( diff --git a/packages/flutter/test/cupertino/scrollbar_test.dart b/packages/flutter/test/cupertino/scrollbar_test.dart index 71dd6c355d44..6d47d2d87acd 100644 --- a/packages/flutter/test/cupertino/scrollbar_test.dart +++ b/packages/flutter/test/cupertino/scrollbar_test.dart @@ -208,10 +208,11 @@ void main() { await tester.pump(); int hapticFeedbackCalls = 0; - SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { if (methodCall.method == 'HapticFeedback.vibrate') { - hapticFeedbackCalls++; + hapticFeedbackCalls += 1; } + return null; }); // Long press on the scrollbar thumb and expect a vibration after it resizes. @@ -966,10 +967,11 @@ void main() { await tester.pump(); int hapticFeedbackCalls = 0; - SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { if (methodCall.method == 'HapticFeedback.vibrate') { - hapticFeedbackCalls++; + hapticFeedbackCalls += 1; } + return null; }); // Long press on the scrollbar thumb and expect a vibration after it resizes. diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 62da023541a5..cddb0cea216b 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -1110,6 +1110,123 @@ void main() { expect(textInput1.width, 200); expect(menu1.width, 200); }); + + testWidgets('Semantics does not include hint when input is not empty', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(); + const String hintText = 'I am hintText'; + TestMenu? selectedValue; + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) => MaterialApp( + theme: themeData, + home: Scaffold( + body: Center( + child: DropdownMenu( + requestFocusOnTap: true, + dropdownMenuEntries: menuChildren, + hintText: hintText, + onSelected: (TestMenu? value) { + setState(() { + selectedValue = value; + }); + }, + controller: controller, + ), + ), + ), + ), + ), + ); + final SemanticsNode node = tester.getSemantics(find.text(hintText)); + + expect(selectedValue?.label, null); + expect(node.label, hintText); + expect(node.value, ''); + + await tester.tap(find.byType(DropdownMenu)); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(MenuItemButton, 'Item 3').last); + await tester.pumpAndSettle(); + expect(selectedValue?.label, 'Item 3'); + expect(node.label, ''); + expect(node.value, 'Item 3'); + }); + + testWidgets('helperText is not visible when errorText is not null', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(); + const String helperText = 'I am helperText'; + const String errorText = 'I am errorText'; + + Widget buildFrame(bool hasError) { + return MaterialApp( + theme: themeData, + home: Scaffold( + body: Center( + child: DropdownMenu( + dropdownMenuEntries: menuChildren, + helperText: helperText, + errorText: hasError ? errorText : null, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(false)); + expect(find.text(helperText), findsOneWidget); + expect(find.text(errorText), findsNothing); + + await tester.pumpWidget(buildFrame(true)); + await tester.pumpAndSettle(); + expect(find.text(helperText), findsNothing); + expect(find.text(errorText), findsOneWidget); + }); + + testWidgets('DropdownMenu can respect helperText when helperText is not null', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(); + const String helperText = 'I am helperText'; + + Widget buildFrame() { + return MaterialApp( + theme: themeData, + home: Scaffold( + body: Center( + child: DropdownMenu( + dropdownMenuEntries: menuChildren, + helperText: helperText, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + expect(find.text(helperText), findsOneWidget); + }); + + testWidgets('DropdownMenu can respect errorText when errorText is not null', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(); + const String errorText = 'I am errorText'; + + Widget buildFrame() { + return MaterialApp( + theme: themeData, + home: Scaffold( + body: Center( + child: DropdownMenu( + dropdownMenuEntries: menuChildren, + errorText: errorText, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame()); + expect(find.text(errorText), findsOneWidget); + }); } enum TestMenu { diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart index be05ac478ef8..0acd34706bae 100644 --- a/packages/flutter/test/material/menu_anchor_test.dart +++ b/packages/flutter/test/material/menu_anchor_test.dart @@ -2481,6 +2481,105 @@ void main() { ); }); + testWidgets('vertically constrained menus are positioned above the anchor by default', (WidgetTester tester) async { + await changeSurfaceSize(tester, const Size(800, 600)); + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.bottomLeft, + child: MenuAnchor( + menuChildren: const [ + MenuItemButton(child: Text('Button1'), + ), + ], + builder: (BuildContext context, MenuController controller, Widget? child) { + return FilledButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); + }, + ), + ), + ); + + await tester.pump(); + await tester.tap(find.text('Tap me')); + await tester.pump(); + + expect(find.byType(MenuItemButton), findsNWidgets(1)); + // Test the default offset (0, 0) vertical position. + expect( + collectSubmenuRects(), + equals(const [ + Rect.fromLTRB(0.0, 488.0, 122.0, 552.0), + ]), + ); + }); + + testWidgets('vertically constrained menus are positioned above the anchor with the provided offset', (WidgetTester tester) async { + await changeSurfaceSize(tester, const Size(800, 600)); + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.bottomLeft, + child: MenuAnchor( + alignmentOffset: const Offset(0, 50), + menuChildren: const [ + MenuItemButton(child: Text('Button1'), + ), + ], + builder: (BuildContext context, MenuController controller, Widget? child) { + return FilledButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text('Tap me'), + ); + }, + ), + ), + ); + }, + ), + ), + ); + + await tester.pump(); + await tester.tap(find.text('Tap me')); + await tester.pump(); + + expect(find.byType(MenuItemButton), findsNWidgets(1)); + // Test the offset (0, 50) vertical position. + expect( + collectSubmenuRects(), + equals(const [ + Rect.fromLTRB(0.0, 438.0, 122.0, 502.0), + ]), + ); + }); + Future buildDensityPaddingApp(WidgetTester tester, { required TextDirection textDirection, VisualDensity visualDensity = VisualDensity.standard, diff --git a/packages/flutter/test/material/paginated_data_table_test.dart b/packages/flutter/test/material/paginated_data_table_test.dart index 0e72cbc4abeb..b68223c4b18b 100644 --- a/packages/flutter/test/material/paginated_data_table_test.dart +++ b/packages/flutter/test/material/paginated_data_table_test.dart @@ -2,13 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(gspencergoog): Remove this tag once this test's state leaks/test -// dependencies have been fixed. -// https://github.com/flutter/flutter/issues/85160 -// Fails with "flutter test --test-randomize-ordering-seed=1000" -@Tags(['no-shuffle']) -library; - import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -845,6 +838,7 @@ void main() { testWidgets('PaginatedDataTable with optional column checkbox', (WidgetTester tester) async { await binding.setSurfaceSize(const Size(800, 800)); + addTearDown(() => binding.setSurfaceSize(null)); Widget buildTable(bool checkbox) => MaterialApp( home: PaginatedDataTable( @@ -1004,6 +998,7 @@ void main() { testWidgets('PaginatedDataTable arrowHeadColor set properly', (WidgetTester tester) async { await binding.setSurfaceSize(const Size(800, 800)); + addTearDown(() => binding.setSurfaceSize(null)); const Color arrowHeadColor = Color(0xFFE53935); await tester.pumpWidget( diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0eebef090ae6..3853a182e242 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -6744,8 +6744,8 @@ void main() { variant: KeySimulatorTransitModeVariant.all() ); - // Regressing test for https://github.com/flutter/flutter/issues/78219 - testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/78219 + testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final TextEditingController controller = TextEditingController(); final TextField textField = TextField( @@ -6778,7 +6778,7 @@ void main() { await tester.tap(find.byType(TextField)); await tester.pumpAndSettle(); - // This setter will set `selection` invalid. + // Clear the text. controller.text = ''; // Paste clipboardContent to the text field. @@ -6790,10 +6790,12 @@ void main() { await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); await tester.pumpAndSettle(); - // Do nothing. - expect(find.text(clipboardContent), findsNothing); - expect(controller.selection, const TextSelection.collapsed(offset: -1)); - }, variant: KeySimulatorTransitModeVariant.all()); + // Clipboard content is correctly pasted. + expect(find.text(clipboardContent), findsOneWidget); + }, + skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events. + variant: KeySimulatorTransitModeVariant.all(), + ); testWidgets('Cut test', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index 451b20223527..d1a46fcb770a 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -468,6 +468,19 @@ void main() { } }, variant: TargetPlatformVariant.all()); + testWidgets('VisualDensity.getDensityForPlatform returns adaptive values', (WidgetTester tester) async { + switch (debugDefaultTargetPlatformOverride!) { + case TargetPlatform.android: + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + expect(VisualDensity.defaultDensityForPlatform(debugDefaultTargetPlatformOverride!), equals(VisualDensity.standard)); + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + expect(VisualDensity.defaultDensityForPlatform(debugDefaultTargetPlatformOverride!), equals(VisualDensity.compact)); + } + }, variant: TargetPlatformVariant.all()); + testWidgets('VisualDensity in ThemeData defaults to "compact" on desktop and "standard" on mobile', (WidgetTester tester) async { final ThemeData themeData = ThemeData(); switch (debugDefaultTargetPlatformOverride!) { @@ -482,6 +495,20 @@ void main() { } }, variant: TargetPlatformVariant.all()); + testWidgets('VisualDensity in ThemeData defaults to the right thing when a platform is supplied to it', (WidgetTester tester) async { + final ThemeData themeData = ThemeData(platform: debugDefaultTargetPlatformOverride! == TargetPlatform.android ? TargetPlatform.linux : TargetPlatform.android); + switch (debugDefaultTargetPlatformOverride!) { + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + expect(themeData.visualDensity, equals(VisualDensity.standard)); + case TargetPlatform.android: + expect(themeData.visualDensity, equals(VisualDensity.compact)); + } + }, variant: TargetPlatformVariant.all()); + testWidgets('Ensure Visual Density effective constraints are clamped', (WidgetTester tester) async { const BoxConstraints square = BoxConstraints.tightFor(width: 35, height: 35); BoxConstraints expanded = const VisualDensity(horizontal: 4.0, vertical: 4.0).effectiveConstraints(square); diff --git a/packages/flutter/test/widgets/automatic_keep_alive_test.dart b/packages/flutter/test/widgets/automatic_keep_alive_test.dart index 0c337a4c79ac..1daf6f078267 100644 --- a/packages/flutter/test/widgets/automatic_keep_alive_test.dart +++ b/packages/flutter/test/widgets/automatic_keep_alive_test.dart @@ -557,6 +557,26 @@ void main() { expect(alternate.children.length, 1); }); + + testWidgets('Keep alive Listenable has its listener removed once called', (WidgetTester tester) async { + final LeakCheckerHandle handle = LeakCheckerHandle(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: ListView.builder( + itemCount: 1, + itemBuilder: (BuildContext context, int index) { + return const KeepAliveListenableLeakChecker(key: GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0)); + }, + ), + )); + final _KeepAliveListenableLeakCheckerState state = const GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0).currentState!; + + expect(handle.hasListeners, false); + state.dispatch(handle); + expect(handle.hasListeners, true); + handle.notifyListeners(); + expect(handle.hasListeners, false); + }); } class _AlwaysKeepAlive extends StatefulWidget { @@ -633,3 +653,26 @@ class RenderSliverMultiBoxAdaptorAlt extends RenderSliver with @override void performLayout() { } } + +class LeakCheckerHandle with ChangeNotifier { + @override + bool get hasListeners => super.hasListeners; +} + +class KeepAliveListenableLeakChecker extends StatefulWidget { + const KeepAliveListenableLeakChecker({super.key}); + + @override + State createState() => _KeepAliveListenableLeakCheckerState(); +} + +class _KeepAliveListenableLeakCheckerState extends State { + void dispatch(Listenable handle) { + KeepAliveNotification(handle).dispatch(context); + } + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart index 4e1edc663482..73c517b6c78a 100644 --- a/packages/flutter/test/widgets/editable_text_shortcuts_test.dart +++ b/packages/flutter/test/widgets/editable_text_shortcuts_test.dart @@ -90,38 +90,6 @@ void main() { ); } - testWidgets( - 'Movement/Deletion shortcuts do nothing when the selection is invalid', - (WidgetTester tester) async { - await tester.pumpWidget(buildEditableText()); - controller.text = testText; - controller.selection = const TextSelection.collapsed(offset: -1); - await tester.pump(); - - const List triggers = [ - LogicalKeyboardKey.backspace, - LogicalKeyboardKey.delete, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.pageUp, - LogicalKeyboardKey.pageDown, - LogicalKeyboardKey.home, - LogicalKeyboardKey.end, - ]; - - for (final SingleActivator activator in triggers.expand(allModifierVariants)) { - await sendKeyCombination(tester, activator); - await tester.pump(); - expect(controller.text, testText, reason: activator.toString()); - expect(controller.selection, const TextSelection.collapsed(offset: -1), reason: activator.toString()); - } - }, - skip: kIsWeb, // [intended] on web these keys are handled by the browser. - variant: TargetPlatformVariant.all(), - ); - group('Common text editing shortcuts: ', () { final TargetPlatformVariant allExceptApple = TargetPlatformVariant.all(excluding: {TargetPlatform.macOS, TargetPlatform.iOS}); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 4de8856cfbb1..94f09e34d0f5 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -55,6 +55,13 @@ enum HandlePositionInViewport { typedef _VoidFutureCallback = Future Function(); +TextEditingValue collapsedAtEnd(String text) { + return TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: text.length), + ); +} + void main() { final MockClipboard mockClipboard = MockClipboard(); TestWidgetsFlutterBinding.ensureInitialized() @@ -835,7 +842,7 @@ void main() { testWidgets('selection rects re-sent when refocused', (WidgetTester tester) async { final List> log = >[]; - SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { if (methodCall.method == 'TextInput.setSelectionRects') { final List args = methodCall.arguments as List; final List selectionRects = []; @@ -847,6 +854,7 @@ void main() { } log.add(selectionRects); } + return null; }); final TextEditingController controller = TextEditingController(); @@ -1725,17 +1733,20 @@ void main() { group('BrowserContextMenu', () { setUp(() async { - SystemChannels.contextMenu.setMockMethodCallHandler((MethodCall call) { - // Just complete successfully, so that BrowserContextMenu thinks that - // the engine successfully received its call. - return Future.value(); - }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.contextMenu, + (MethodCall call) { + // Just complete successfully, so that BrowserContextMenu thinks that + // the engine successfully received its call. + return Future.value(); + }, + ); await BrowserContextMenu.disableContextMenu(); }); tearDown(() async { await BrowserContextMenu.enableContextMenu(); - SystemChannels.contextMenu.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null); }); testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { @@ -5162,7 +5173,7 @@ void main() { tester.view.physicalSize = const Size(750.0, 1334.0); final List> log = >[]; - SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) { if (methodCall.method == 'TextInput.setSelectionRects') { final List args = methodCall.arguments as List; final List selectionRects = []; @@ -5174,6 +5185,7 @@ void main() { } log.add(selectionRects); } + return null; }); final TextEditingController controller = TextEditingController(); @@ -5297,8 +5309,9 @@ void main() { testWidgets('selection rects are not sent if scribbleEnabled is false', (WidgetTester tester) async { final List log = []; - SystemChannels.textInput.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async { log.add(methodCall); + return null; }); final TextEditingController controller = TextEditingController(); @@ -5635,7 +5648,7 @@ void main() { tester.testTextInput.log.clear(); - controller.value = TextEditingValue(text: 'a' * 100, composing: const TextRange(start: 0, end: 10)); + controller.value = collapsedAtEnd('a' * 100).copyWith(composing: const TextRange(start: 0, end: 10)); await tester.pump(); expect(tester.testTextInput.log, contains( @@ -9441,86 +9454,95 @@ void main() { }); group('EditableText does not send editing values more than once', () { - final TextEditingController controller = TextEditingController(text: testText); - final EditableText editableText = EditableText( - showSelectionHandles: true, - maxLines: 2, - controller: controller, - focusNode: FocusNode(), - cursorColor: Colors.red, - backgroundCursorColor: Colors.blue, - style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), - keyboardType: TextInputType.text, - inputFormatters: [LengthLimitingTextInputFormatter(6)], - onChanged: (String s) => controller.text += ' onChanged', - ); + Widget boilerplate(TextEditingController controller) { + final EditableText editableText = EditableText( + showSelectionHandles: true, + maxLines: 2, + controller: controller, + focusNode: FocusNode(), + cursorColor: Colors.red, + backgroundCursorColor: Colors.blue, + style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), + keyboardType: TextInputType.text, + inputFormatters: [LengthLimitingTextInputFormatter(6)], + onChanged: (String s) { + controller.value = collapsedAtEnd('${controller.text} onChanged'); + }, + ); - final Widget widget = MediaQuery( - data: const MediaQueryData(), - child: Directionality( - textDirection: TextDirection.ltr, - child: editableText, - ), - ); + controller.addListener(() { + if (!controller.text.endsWith('listener')) { + controller.value = collapsedAtEnd('${controller.text} listener'); + } + }); - controller.addListener(() { - if (!controller.text.endsWith('listener')) { - controller.text += ' listener'; - } - }); + return MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: editableText, + ), + ); + } testWidgets('input from text input plugin', (WidgetTester tester) async { - await tester.pumpWidget(widget); + final TextEditingController controller = TextEditingController(text: testText); + await tester.pumpWidget(boilerplate(controller)); // Connect. await tester.showKeyboard(find.byType(EditableText)); tester.testTextInput.log.clear(); - final EditableTextState state = tester.state(find.byWidget(editableText)); - state.updateEditingValue(const TextEditingValue(text: 'remoteremoteremote')); + final EditableTextState state = tester.state(find.byType(EditableText)); + state.updateEditingValue(collapsedAtEnd('remoteremoteremote')); // Apply in order: length formatter -> listener -> onChanged -> listener. - expect(controller.text, 'remote listener onChanged listener'); + const String expectedText = 'remote listener onChanged listener'; + expect(controller.text, expectedText); final List updates = tester.testTextInput.log .where((MethodCall call) => call.method == 'TextInput.setEditingState') .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map)) .toList(growable: false); - expect(updates, const [TextEditingValue(text: 'remote listener onChanged listener')]); + expect(updates, [collapsedAtEnd(expectedText)]); tester.testTextInput.log.clear(); // If by coincidence the text input plugin sends the same value back, // do nothing. - state.updateEditingValue(const TextEditingValue(text: 'remote listener onChanged listener')); + state.updateEditingValue(collapsedAtEnd(expectedText)); expect(controller.text, 'remote listener onChanged listener'); expect(tester.testTextInput.log, isEmpty); }); testWidgets('input from text selection menu', (WidgetTester tester) async { - await tester.pumpWidget(widget); + final TextEditingController controller = TextEditingController(text: testText); + await tester.pumpWidget(boilerplate(controller)); // Connect. await tester.showKeyboard(find.byType(EditableText)); tester.testTextInput.log.clear(); - final EditableTextState state = tester.state(find.byWidget(editableText)); - state.userUpdateTextEditingValue(const TextEditingValue(text: 'remoteremoteremote'), SelectionChangedCause.keyboard); + final EditableTextState state = tester.state(find.byType(EditableText)); + state.userUpdateTextEditingValue( + collapsedAtEnd('remoteremoteremote'), + SelectionChangedCause.keyboard, + ); - // Apply in order: length formatter -> listener -> onChanged -> listener. - expect(controller.text, 'remote listener onChanged listener'); final List updates = tester.testTextInput.log .where((MethodCall call) => call.method == 'TextInput.setEditingState') .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map)) .toList(growable: false); - expect(updates, const [TextEditingValue(text: 'remote listener onChanged listener')]); + const String expectedText = 'remote listener onChanged listener'; + expect(updates, [collapsedAtEnd(expectedText)]); tester.testTextInput.log.clear(); }); testWidgets('input from controller', (WidgetTester tester) async { - await tester.pumpWidget(widget); + final TextEditingController controller = TextEditingController(text: testText); + await tester.pumpWidget(boilerplate(controller)); // Connect. await tester.showKeyboard(find.byType(EditableText)); @@ -9532,7 +9554,7 @@ void main() { .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map)) .toList(growable: false); - expect(updates, const [TextEditingValue(text: 'remoteremoteremote listener')]); + expect(updates, [collapsedAtEnd('remoteremoteremote listener')]); }); testWidgets('input from changing controller', (WidgetTester tester) async { @@ -9819,7 +9841,7 @@ void main() { )); await tester.showKeyboard(find.byType(EditableText)); - controller.text += '...'; + controller.value = collapsedAtEnd('${controller.text}...'); await tester.idle(); final List logOrder = [ @@ -9852,7 +9874,7 @@ void main() { }); final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { if (newValue.text == 'I will be modified by the formatter.') { - newValue = const TextEditingValue(text: 'Flutter is the best!'); + newValue = collapsedAtEnd('Flutter is the best!'); } return newValue; }); @@ -9919,8 +9941,8 @@ void main() { methodCall, isMethodCall('TextInput.setEditingState', arguments: { 'text': 'Flutter is the best!', - 'selectionBase': -1, - 'selectionExtent': -1, + 'selectionBase': 20, + 'selectionExtent': 20, 'selectionAffinity': 'TextAffinity.downstream', 'selectionIsDirectional': false, 'composingBase': -1, @@ -9931,8 +9953,9 @@ void main() { log.clear(); // setEditingState is called when the [controller.value] is modified by local. + String text = 'I love flutter!'; setState(() { - controller.text = 'I love flutter!'; + controller.value = collapsedAtEnd(text); }); expect(log.length, 1); methodCall = log[0]; @@ -9940,8 +9963,8 @@ void main() { methodCall, isMethodCall('TextInput.setEditingState', arguments: { 'text': 'I love flutter!', - 'selectionBase': -1, - 'selectionExtent': -1, + 'selectionBase': text.length, + 'selectionExtent': text.length, 'selectionAffinity': 'TextAffinity.downstream', 'selectionIsDirectional': false, 'composingBase': -1, @@ -9953,8 +9976,9 @@ void main() { // Currently `_receivedRemoteTextEditingValue` equals 'I will be modified by the formatter.', // setEditingState will be called when set the [controller.value] to `_receivedRemoteTextEditingValue` by local. + text = 'I will be modified by the formatter.'; setState(() { - controller.text = 'I will be modified by the formatter.'; + controller.value = collapsedAtEnd(text); }); expect(log.length, 1); methodCall = log[0]; @@ -9962,8 +9986,8 @@ void main() { methodCall, isMethodCall('TextInput.setEditingState', arguments: { 'text': 'I will be modified by the formatter.', - 'selectionBase': -1, - 'selectionExtent': -1, + 'selectionBase': text.length, + 'selectionExtent': text.length, 'selectionAffinity': 'TextAffinity.downstream', 'selectionIsDirectional': false, 'composingBase': -1, @@ -9980,7 +10004,7 @@ void main() { return null; }); final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { - return const TextEditingValue(text: 'Flutter is the best!'); + return collapsedAtEnd('Flutter is the best!'); }); final TextEditingController controller = TextEditingController(); @@ -10026,11 +10050,8 @@ void main() { final EditableTextState state = tester.firstState(find.byType(EditableText)); // setEditingState is called when remote value modified by the formatter. - state.updateEditingValue(TextEditingValue( - text: 'I will be modified by the formatter.', - selection: controller.selection, - )); - expect(log.length, 1); + state.updateEditingValue(collapsedAtEnd('I will be modified by the formatter.')); + expect(log.length, 2); expect(log, contains(matchesMethodCall( 'TextInput.setEditingState', args: allOf( @@ -10040,10 +10061,8 @@ void main() { log.clear(); - state.updateEditingValue(const TextEditingValue( - text: 'I will be modified by the formatter.', - )); - expect(log.length, 1); + state.updateEditingValue(collapsedAtEnd('I will be modified by the formatter.')); + expect(log.length, 2); expect(log, contains(matchesMethodCall( 'TextInput.setEditingState', args: allOf( @@ -10525,9 +10544,10 @@ void main() { expect(state.wantKeepAlive, true); expect(formatter.formatCallCount, 0); - state.updateEditingValue(const TextEditingValue(text: 'test')); - state.updateEditingValue(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 2))); - state.updateEditingValue(const TextEditingValue(text: '0')); // pass to formatter once to check the values. + state.updateEditingValue(collapsedAtEnd('test')); + state.updateEditingValue(collapsedAtEnd('test').copyWith(composing: const TextRange(start: 1, end: 2))); + // Pass to formatter once to check the values. + state.updateEditingValue(collapsedAtEnd('test')); expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2)); expect(formatter.lastOldValue.text, 'test'); }); @@ -12997,7 +13017,7 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async await sendUndo(tester); expect(controller.value, composingStep2); - // Waiting for the throttling beetween undos should have no effect. + // Waiting for the throttling between undos should have no effect. await tester.pump(const Duration(milliseconds: 500)); // Undo second insertion. @@ -15522,6 +15542,65 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ); }); + testWidgets('Selection is updated when the field has focus and the new selection is invalid', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/120631. + final TextEditingController controller = TextEditingController(); + controller.text = 'Text'; + final FocusNode focusNode = FocusNode(); + + await tester.pumpWidget( + MaterialApp( + home: EditableText( + key: ValueKey(controller.text), + controller: controller, + focusNode: focusNode, + style: Typography.material2018().black.titleMedium!, + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + ); + + expect(focusNode.hasFocus, isFalse); + expect( + controller.selection, + const TextSelection.collapsed(offset: -1), + ); + + // Tab to focus the field. + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pumpAndSettle(); + expect(focusNode.hasFocus, isTrue); + expect( + controller.selection, + kIsWeb + ? TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ) + : TextSelection.collapsed( + offset: controller.text.length, + ), + ); + + // Update text without specifying the selection. + controller.text = 'Updated'; + + // As the TextField is focused the selection should be automatically adjusted. + expect(focusNode.hasFocus, isTrue); + expect( + controller.selection, + kIsWeb + ? TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ) + : TextSelection.collapsed( + offset: controller.text.length, + ), + ); + }); + testWidgets('when having focus stolen between frames on web', (WidgetTester tester) async { final TextEditingController controller1 = TextEditingController(); controller1.text = 'Text1'; diff --git a/packages/flutter/test/widgets/selectable_region_test.dart b/packages/flutter/test/widgets/selectable_region_test.dart index 3abe91a6045d..45625821af17 100644 --- a/packages/flutter/test/widgets/selectable_region_test.dart +++ b/packages/flutter/test/widgets/selectable_region_test.dart @@ -621,11 +621,13 @@ void main() { expect(paragraph1.selections.isEmpty, isTrue); expect(paragraph2.selections.isEmpty, isTrue); - // Reset selection and focus selectable region. - controller.selection = const TextSelection.collapsed(offset: -1); + // Focus selectable region. selectableRegionFocus.requestFocus(); await tester.pump(); + // Reset controller selection once the TextField is unfocused. + controller.selection = const TextSelection.collapsed(offset: -1); + // Make sure keyboard select all will be handled by selectable region now. await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true)); expect(controller.selection, const TextSelection.collapsed(offset: -1)); @@ -672,11 +674,13 @@ void main() { expect(paragraph1.selections.isEmpty, isTrue); expect(paragraph2.selections.isEmpty, isTrue); - // Reset selection and focus selectable region. - controller.selection = const TextSelection.collapsed(offset: -1); + // Focus selectable region. selectableRegionFocus.requestFocus(); await tester.pump(); + // Reset controller selection once the TextField is unfocused. + controller.selection = const TextSelection.collapsed(offset: -1); + // Make sure keyboard select all will be handled by selectable region now. await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true)); expect(controller.selection, const TextSelection.collapsed(offset: -1)); @@ -1883,7 +1887,7 @@ void main() { group('BrowserContextMenu', () { setUp(() async { - SystemChannels.contextMenu.setMockMethodCallHandler((MethodCall call) { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, (MethodCall call) { // Just complete successfully, so that BrowserContextMenu thinks that // the engine successfully received its call. return Future.value(); @@ -1893,7 +1897,7 @@ void main() { tearDown(() async { await BrowserContextMenu.enableContextMenu(); - SystemChannels.contextMenu.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null); }); testWidgets('web can show flutter context menu when the browser context menu is disabled', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/undo_history_test.dart b/packages/flutter/test/widgets/undo_history_test.dart index 9d2a8d0b66ab..ce09b652e4d6 100644 --- a/packages/flutter/test/widgets/undo_history_test.dart +++ b/packages/flutter/test/widgets/undo_history_test.dart @@ -311,8 +311,9 @@ void main() { testWidgets('changes should send setUndoState to the UndoManagerConnection on iOS', (WidgetTester tester) async { final List log = []; - SystemChannels.undoManager.setMockMethodCallHandler((MethodCall methodCall) async { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.undoManager, (MethodCall methodCall) async { log.add(methodCall); + return null; }); final FocusNode focusNode = FocusNode(); diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index bcc629a6e07d..d6b7fbdd1b6d 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -505,6 +505,17 @@ abstract class TestWidgetsFlutterBinding extends BindingBase /// then flushes microtasks. /// /// Set to null to use the default surface size. + /// + /// To avoid affecting other tests by leaking state, a test that + /// uses this method should always reset the surface size to the default. + /// For example, using `addTearDown`: + /// ```dart + /// await binding.setSurfaceSize(someSize); + /// addTearDown(() => binding.setSurfaceSize(null)); + /// ``` + /// + /// See also [TestFlutterView.physicalSize], which has a similar effect. + // TODO(pdblasi-google): Deprecate this. https://github.com/flutter/flutter/issues/123881 Future setSurfaceSize(Size? size) { return TestAsyncUtils.guard(() async { assert(inTest); diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 1aa29aa7981c..8617e08a0c75 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -1133,6 +1133,19 @@ abstract class WidgetController { return result; } + TestGesture _createGesture({ + int? pointer, + required PointerDeviceKind kind, + required int buttons, + }) { + return TestGesture( + dispatcher: sendEventToBinding, + kind: kind, + pointer: pointer ?? _getNextPointer(), + buttons: buttons, + ); + } + /// Creates gesture and returns the [TestGesture] object which you can use /// to continue the gesture using calls on the [TestGesture] object. /// @@ -1143,12 +1156,7 @@ abstract class WidgetController { PointerDeviceKind kind = PointerDeviceKind.touch, int buttons = kPrimaryButton, }) async { - return TestGesture( - dispatcher: sendEventToBinding, - kind: kind, - pointer: pointer ?? _getNextPointer(), - buttons: buttons, - ); + return _createGesture(pointer: pointer, kind: kind, buttons: buttons); } /// Creates a gesture with an initial appropriate starting gesture at a @@ -1172,11 +1180,7 @@ abstract class WidgetController { PointerDeviceKind kind = PointerDeviceKind.touch, int buttons = kPrimaryButton, }) async { - final TestGesture result = await createGesture( - pointer: pointer, - kind: kind, - buttons: buttons, - ); + final TestGesture result = _createGesture(pointer: pointer, kind: kind, buttons: buttons); if (kind == PointerDeviceKind.trackpad) { await result.panZoomStart(downLocation); } else { diff --git a/packages/flutter_test/lib/src/deprecated.dart b/packages/flutter_test/lib/src/deprecated.dart index cc43e7f6071f..8c4194b888d0 100644 --- a/packages/flutter_test/lib/src/deprecated.dart +++ b/packages/flutter_test/lib/src/deprecated.dart @@ -6,8 +6,6 @@ import 'package:flutter/services.dart'; import 'binding.dart'; -// TODO(ianh): Once cocoon and other customer_tests are migrated, deprecate these transitional APIs - /// Shim to support the obsolete [setMockMessageHandler] and /// [checkMockMessageHandler] methods on [BinaryMessenger] in tests. /// @@ -19,21 +17,23 @@ import 'binding.dart'; /// more accurately represents the actual method invocation. extension TestBinaryMessengerExtension on BinaryMessenger { /// Shim for [TestDefaultBinaryMessenger.setMockMessageHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.setMockMessageHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler instead. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.setMockMessageHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler instead. ' + 'For the first argument, pass channel.name. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) void setMockMessageHandler(String channel, MessageHandler? handler) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMessageHandler(channel, handler); } /// Shim for [TestDefaultBinaryMessenger.checkMockMessageHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead.' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead. ' + 'For the first argument, pass channel.name. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) bool checkMockMessageHandler(String channel, Object? handler) { return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(channel, handler); } @@ -49,22 +49,23 @@ extension TestBinaryMessengerExtension on BinaryMessenger { /// directly. This more accurately represents the actual method invocation. extension TestBasicMessageChannelExtension on BasicMessageChannel { /// Shim for [TestDefaultBinaryMessenger.setMockDecodedMessageHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler instead. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler instead. ' + 'Pass the channel as the first argument. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) void setMockMessageHandler(Future Function(T? message)? handler) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockDecodedMessageHandler(this, handler); } /// Shim for [TestDefaultBinaryMessenger.checkMockMessageHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead. ' - // 'For the first argument, pass channel.name. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead. ' + 'For the first argument, pass channel.name. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) bool checkMockMessageHandler(Object? handler) { return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(name, handler); } @@ -80,22 +81,23 @@ extension TestBasicMessageChannelExtension on BasicMessageChannel { /// This more accurately represents the actual method invocation. extension TestMethodChannelExtension on MethodChannel { /// Shim for [TestDefaultBinaryMessenger.setMockMethodCallHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.setMockMethodCallHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler instead. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.setMockMethodCallHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler instead. ' + 'Pass the channel as the first argument. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) void setMockMethodCallHandler(Future? Function(MethodCall call)? handler) { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(this, handler); } /// Shim for [TestDefaultBinaryMessenger.checkMockMessageHandler]. - // TODO(ianh): deprecate this method: @NotYetDeprecated( - // 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' - // 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead. ' - // 'For the first argument, pass channel.name. ' - // 'This feature was deprecated after v2.1.0-10.0.pre.' - // ) + @Deprecated( + 'Use tester.binding.defaultBinaryMessenger.checkMockMessageHandler or ' + 'TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler instead. ' + 'For the first argument, pass channel.name. ' + 'This feature was deprecated after v3.9.0-19.0.pre.' + ) bool checkMockMethodCallHandler(Object? handler) { return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(name, handler); } diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index 9536aa881602..fb7256cf79af 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart @@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'binding.dart'; -import 'deprecated.dart'; import 'test_async_utils.dart'; import 'test_text_input_key_handler.dart'; @@ -59,7 +58,7 @@ class TestTextInput { /// /// Called by the binding at the top of a test when /// [TestWidgetsFlutterBinding.registerTestTextInput] is true. - void register() => SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall); + void register() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, _handleTextInputCall); /// Removes this object as a mock handler for [SystemChannels.textInput]. /// @@ -68,13 +67,13 @@ class TestTextInput { /// /// Called by the binding at the end of a (successful) test when /// [TestWidgetsFlutterBinding.registerTestTextInput] is true. - void unregister() => SystemChannels.textInput.setMockMethodCallHandler(null); + void unregister() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, null); /// Whether this [TestTextInput] is registered with [SystemChannels.textInput]. /// /// The binding uses the [register] and [unregister] methods to control this /// value when [TestWidgetsFlutterBinding.registerTestTextInput] is true. - bool get isRegistered => SystemChannels.textInput.checkMockMethodCallHandler(_handleTextInputCall); + bool get isRegistered => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler(SystemChannels.textInput.name, _handleTextInputCall); int? _client; diff --git a/packages/flutter_test/test/controller_test.dart b/packages/flutter_test/test/controller_test.dart index 9e73da96fd6c..9d060a23c5a4 100644 --- a/packages/flutter_test/test/controller_test.dart +++ b/packages/flutter_test/test/controller_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:stack_trace/stack_trace.dart'; class TestDragData { const TestDragData( @@ -555,6 +556,34 @@ void main() { }, ); + testWidgets( + 'WidgetTester.tap appears in stack trace on error', + (WidgetTester tester) async { + // Regression test from https://github.com/flutter/flutter/pull/123946 + await tester.pumpWidget( + const MaterialApp(home: Scaffold(body: Text('target')))); + + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.text('target')), pointer: 1); + addTearDown(() => gesture.up()); + + Trace? stackTrace; + try { + await tester.tap(find.text('target'), pointer: 1); + } on Error catch (e) { + stackTrace = Trace.from(e.stackTrace!); + } + expect(stackTrace, isNotNull); + + final int tapFrame = stackTrace!.frames.indexWhere( + (Frame frame) => frame.member == 'WidgetController.tap'); + expect(tapFrame, greaterThanOrEqualTo(0)); + expect(stackTrace.frames[tapFrame].package, 'flutter_test'); + expect(stackTrace.frames[tapFrame+1].member, 'main.'); + expect(stackTrace.frames[tapFrame+1].package, null); + }, + ); + testWidgets( 'ensureVisible: scrolls to make widget visible', (WidgetTester tester) async { diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart index 0f2ff1b4cedd..7aad6d99134f 100644 --- a/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart +++ b/packages/flutter_tools/lib/src/localizations/gen_l10n_types.dart @@ -94,6 +94,7 @@ const Set _validNumberFormats = { 'compactLong', 'currency', 'decimalPattern', + 'decimalPatternDigits', 'decimalPercentPattern', 'percentPattern', 'scientificPattern', @@ -118,6 +119,7 @@ const Set _numberFormatsWithNamedParameters = { 'compactSimpleCurrency', 'compactLong', 'currency', + 'decimalPatternDigits', 'decimalPercentPattern', 'simpleCurrency', }; diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index e13eb94f3142..c6f534a9896a 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -97,7 +97,7 @@ class FlutterCommandResult { } /// Common flutter command line options. -class FlutterOptions { +abstract final class FlutterOptions { static const String kExtraFrontEndOptions = 'extra-front-end-options'; static const String kExtraGenSnapshotOptions = 'extra-gen-snapshot-options'; static const String kEnableExperiment = 'enable-experiment'; diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index f748b9e1227c..e98557a61797 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -119,7 +119,11 @@ class WebBuilder { } finally { status.stop(); } - _flutterUsage.sendTiming('build', 'dart2js', Duration(milliseconds: sw.elapsedMilliseconds)); + _flutterUsage.sendTiming( + 'build', + compilerConfig.isWasm ? 'dart2wasm' : 'dart2js', + Duration(milliseconds: sw.elapsedMilliseconds), + ); } } diff --git a/packages/flutter_tools/templates/plugin/test/projectName_method_channel_test.dart.tmpl b/packages/flutter_tools/templates/plugin/test/projectName_method_channel_test.dart.tmpl index c4a43991425a..ca26ef2a2899 100644 --- a/packages/flutter_tools/templates/plugin/test/projectName_method_channel_test.dart.tmpl +++ b/packages/flutter_tools/templates/plugin/test/projectName_method_channel_test.dart.tmpl @@ -3,19 +3,22 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:{{projectName}}/{{projectName}}_method_channel.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + MethodChannel{{pluginDartClass}} platform = MethodChannel{{pluginDartClass}}(); const MethodChannel channel = MethodChannel('{{projectName}}'); - TestWidgetsFlutterBinding.ensureInitialized(); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); }); tearDown(() { - channel.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); }); test('getPlatformVersion', () async { diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart index 61357e01c90d..0ed3498d7b02 100644 --- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart +++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart @@ -3196,4 +3196,52 @@ AppLocalizations lookupAppLocalizations(Locale locale) { ..writeOutputFiles(); expect(logger.hadWarningOutput, isFalse); }); + + testWithoutContext('can use decimalPatternDigits with decimalDigits optional parameter', () { + const String arbFile = ''' +{ + "treeHeight": "Tree height is {height}m.", + "@treeHeight": { + "placeholders": { + "height": { + "type": "double", + "format": "decimalPatternDigits", + "optionalParameters": { + "decimalDigits": 3 + } + } + } + } +}'''; + + final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n') + ..createSync(recursive: true); + l10nDirectory.childFile(defaultTemplateArbFileName) + .writeAsStringSync(arbFile); + + LocalizationsGenerator( + fileSystem: fs, + inputPathString: defaultL10nPathString, + outputPathString: defaultL10nPathString, + templateArbFileName: defaultTemplateArbFileName, + outputFileString: defaultOutputFileString, + classNameString: defaultClassNameString, + logger: logger, + ) + ..loadResources() + ..writeOutputFiles(); + + final String localizationsFile = fs.file( + fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'), + ).readAsStringSync(); + expect(localizationsFile, containsIgnoringWhitespace(r''' +String treeHeight(double height) { +''')); + expect(localizationsFile, containsIgnoringWhitespace(r''' +NumberFormat.decimalPatternDigits( + locale: localeName, + decimalDigits: 3 +); +''')); + }); } diff --git a/packages/flutter_tools/test/general.shard/web/compile_web_test.dart b/packages/flutter_tools/test/general.shard/web/compile_web_test.dart index 1f0c0e36bc72..2a37ed35cb12 100644 --- a/packages/flutter_tools/test/general.shard/web/compile_web_test.dart +++ b/packages/flutter_tools/test/general.shard/web/compile_web_test.dart @@ -78,7 +78,7 @@ void main() { // Sends timing event. final TestTimingEvent timingEvent = testUsage.timings.single; expect(timingEvent.category, 'build'); - expect(timingEvent.variableName, 'dart2js'); + expect(timingEvent.variableName, 'dart2wasm'); }); testUsingContext('WebBuilder throws tool exit on failure', () async {