diff --git a/dev/snippets/config/README.md b/dev/snippets/config/README.md index 1c305b7713..d5539eeb56 100644 --- a/dev/snippets/config/README.md +++ b/dev/snippets/config/README.md @@ -4,4 +4,4 @@ The [snippets] tool uses the files in the `skeletons` directory to inject code blocks generated from `{@tool dartpad}`, `{@tool sample}`, and `{@tool snippet}` sections found in doc comments into the API docs. -[snippet]: https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets +[snippets]: https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets diff --git a/examples/api/README.md b/examples/api/README.md index 6e670b7835..e636fa5e66 100644 --- a/examples/api/README.md +++ b/examples/api/README.md @@ -30,6 +30,10 @@ that on an Android device like so: ## Naming +> `lib/library/file/class_name.n.dart` +> +> `lib/library/file/class_name.member_name.n.dart` + The naming scheme for the files is similar to the hierarchy under [packages/flutter/lib/src](../../packages/flutter/lib/src), except that the files are represented as directories (without the `.dart` suffix), and each diff --git a/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart b/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart new file mode 100644 index 0000000000..ea04dd6a85 --- /dev/null +++ b/examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart @@ -0,0 +1,26 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flutter code sample for WidgetsApp + +import 'package:flutter/material.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return WidgetsApp( + title: 'Example', + color: const Color(0xFF000000), + home: const Center(child: Text('Hello World')), + pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) => PageRouteBuilder( + settings: settings, + pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) => builder(context), + ), + ); + } +} diff --git a/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart b/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart new file mode 100644 index 0000000000..603e88f83f --- /dev/null +++ b/examples/api/test/widgets/app/widgets_app.widgets_app.0_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/widgets/app/widgets_app.widgets_app.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('WidgetsApp test', (WidgetTester tester) async { + await tester.pumpWidget( + const example.MyApp(), + ); + + expect(find.text('Hello World'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/cupertino/debug.dart b/packages/flutter/lib/src/cupertino/debug.dart index 294dc27f54..bbdb2b29a1 100644 --- a/packages/flutter/lib/src/cupertino/debug.dart +++ b/packages/flutter/lib/src/cupertino/debug.dart @@ -19,6 +19,10 @@ import 'localizations.dart'; /// assert(debugCheckHasCupertinoLocalizations(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// Does nothing if asserts are disabled. Always returns true. bool debugCheckHasCupertinoLocalizations(BuildContext context) { assert(() { diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart index 38b98f98d9..7ed14958dc 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'button.dart'; @@ -32,8 +31,11 @@ const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.wit darkColor: Color(0xff302928), ); - -class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { +/// Desktop Cupertino styled text selection controls. +/// +/// The [cupertinoDesktopTextSelectionControls] global variable has a +/// suitable instance of this class. +class CupertinoDesktopTextSelectionControls extends TextSelectionControls { /// Desktop has no text selection handles. @override Size getHandleSize(double textLineHeight) { @@ -87,7 +89,7 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { /// Text selection controls that follows Mac design conventions. final TextSelectionControls cupertinoDesktopTextSelectionControls = - _CupertinoDesktopTextSelectionControls(); + CupertinoDesktopTextSelectionControls(); // Generates the child that's passed into CupertinoDesktopTextSelectionToolbar. class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget { diff --git a/packages/flutter/lib/src/cupertino/text_selection.dart b/packages/flutter/lib/src/cupertino/text_selection.dart index 589cf0d3b4..06db922ba5 100644 --- a/packages/flutter/lib/src/cupertino/text_selection.dart +++ b/packages/flutter/lib/src/cupertino/text_selection.dart @@ -5,7 +5,6 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'localizations.dart'; @@ -192,6 +191,9 @@ class _TextSelectionHandlePainter extends CustomPainter { } /// iOS Cupertino styled text selection controls. +/// +/// The [cupertinoTextSelectionControls] global variable has a +/// suitable instance of this class. class CupertinoTextSelectionControls extends TextSelectionControls { /// Returns the size of the Cupertino handle. @override diff --git a/packages/flutter/lib/src/material/debug.dart b/packages/flutter/lib/src/material/debug.dart index 25a8816d2a..7afa028abe 100644 --- a/packages/flutter/lib/src/material/debug.dart +++ b/packages/flutter/lib/src/material/debug.dart @@ -23,6 +23,10 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; /// assert(debugCheckHasMaterial(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This method can be expensive (it walks the element tree). /// /// Does nothing if asserts are disabled. Always returns true. @@ -67,6 +71,10 @@ bool debugCheckHasMaterial(BuildContext context) { /// assert(debugCheckHasMaterialLocalizations(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This function has the side-effect of establishing an inheritance /// relationship with the nearest [Localizations] widget (see /// [BuildContext.dependOnInheritedWidgetOfExactType]). This is ok if the caller @@ -112,6 +120,10 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) { /// assert(debugCheckHasScaffold(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This method can be expensive (it walks the element tree). /// /// Does nothing if asserts are disabled. Always returns true. @@ -145,6 +157,10 @@ bool debugCheckHasScaffold(BuildContext context) { /// assert(debugCheckHasScaffoldMessenger(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This method can be expensive (it walks the element tree). /// /// Does nothing if asserts are disabled. Always returns true. diff --git a/packages/flutter/lib/src/material/desktop_text_selection.dart b/packages/flutter/lib/src/material/desktop_text_selection.dart index 9c55787f9c..a6be9d276e 100644 --- a/packages/flutter/lib/src/material/desktop_text_selection.dart +++ b/packages/flutter/lib/src/material/desktop_text_selection.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -19,7 +18,11 @@ import 'theme.dart'; const double _kToolbarScreenPadding = 8.0; const double _kToolbarWidth = 222.0; -class _DesktopTextSelectionControls extends TextSelectionControls { +/// Desktop Material styled text selection controls. +/// +/// The [desktopTextSelectionControls] global variable has a +/// suitable instance of this class. +class DesktopTextSelectionControls extends TextSelectionControls { /// Desktop has no text selection handles. @override Size getHandleSize(double textLineHeight) { @@ -83,7 +86,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls { /// Text selection controls that loosely follows Material design conventions. final TextSelectionControls desktopTextSelectionControls = - _DesktopTextSelectionControls(); + DesktopTextSelectionControls(); // Generates the child that's passed into DesktopTextSelectionToolbar. class _DesktopTextSelectionControlsToolbar extends StatefulWidget { @@ -145,12 +148,14 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect @override Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + assert(debugCheckHasMediaQuery(context)); + // Don't render the menu until the state of the clipboard is known. if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { return const SizedBox(width: 0.0, height: 0.0); } - assert(debugCheckHasMediaQuery(context)); final MediaQueryData mediaQuery = MediaQuery.of(context); final Offset midpointAnchor = Offset( @@ -161,7 +166,6 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect widget.selectionMidpoint.dy - widget.globalEditableRegion.top, ); - assert(debugCheckHasMaterialLocalizations(context)); final MaterialLocalizations localizations = MaterialLocalizations.of(context); final List items = []; diff --git a/packages/flutter/lib/src/material/selection_area.dart b/packages/flutter/lib/src/material/selection_area.dart index 18000995aa..49f98ef748 100644 --- a/packages/flutter/lib/src/material/selection_area.dart +++ b/packages/flutter/lib/src/material/selection_area.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; +import 'debug.dart'; import 'desktop_text_selection.dart'; import 'magnifier.dart'; import 'text_selection.dart'; @@ -19,6 +20,10 @@ import 'theme.dart'; /// a specific screen, consider wrapping the body of the [Route] with a /// [SelectionArea]. /// +/// The [SelectionArea] widget must have a [Localizations] ancestor that +/// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget +/// ensures that such an ancestor is present. +/// /// {@tool dartpad} /// This example shows how to make a screen selectable. /// @@ -85,6 +90,7 @@ class _SelectionAreaState extends State { @override Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); TextSelectionControls? controls = widget.selectionControls; switch (Theme.of(context).platform) { case TargetPlatform.android: diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart index b090922abb..fc8d86be7b 100644 --- a/packages/flutter/lib/src/material/text_selection.dart +++ b/packages/flutter/lib/src/material/text_selection.dart @@ -5,7 +5,6 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'debug.dart'; @@ -22,6 +21,9 @@ const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0; const double _kToolbarContentDistance = 8.0; /// Android Material styled text selection controls. +/// +/// The [materialTextSelectionControls] global variable has a +/// suitable instance of this class. class MaterialTextSelectionControls extends TextSelectionControls { /// Returns the size of the Material handle. @override diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index e98e54be58..10964ce19d 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -420,6 +420,8 @@ class RenderParagraph extends RenderBox } /// The color to use when painting the selection. + /// + /// Ignored if the text is not selectable (e.g. if [registrar] is null). Color? get selectionColor => _selectionColor; Color? _selectionColor; set selectionColor(Color? value) { diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart index 6ac93473ae..93f8c455f4 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -282,7 +282,7 @@ class WidgetsApp extends StatefulWidget { /// /// If [home] or [routes] are not null, the routing implementation needs to know how /// appropriately build [PageRoute]s. This can be achieved by supplying the - /// [pageRouteBuilder] parameter. The [pageRouteBuilder] is used by [MaterialApp] + /// [pageRouteBuilder] parameter. The [pageRouteBuilder] is used by [MaterialApp] /// and [CupertinoApp] to create [MaterialPageRoute]s and [CupertinoPageRoute], /// respectively. /// @@ -296,15 +296,21 @@ class WidgetsApp extends StatefulWidget { /// and [builder] are null, or if they fail to create a requested route, /// [onGenerateRoute] will be invoked. If that fails, [onUnknownRoute] will be invoked. /// - /// The [pageRouteBuilder] will create a [PageRoute] that wraps newly built routes. + /// The [pageRouteBuilder] is called to create a [PageRoute] that wraps newly built routes. /// If the [builder] is non-null and the [onGenerateRoute] argument is null, then the - /// [builder] will not be provided only with the context and the child widget, whereas - /// the [pageRouteBuilder] will be provided with [RouteSettings]. If [onGenerateRoute] - /// is not provided, [navigatorKey], [onUnknownRoute], [navigatorObservers], and - /// [initialRoute] must have their default values, as they will have no effect. + /// [builder] will be provided only with the context and the child widget, whereas + /// the [pageRouteBuilder] will be provided with [RouteSettings]; in that configuration, + /// the [navigatorKey], [onUnknownRoute], [navigatorObservers], and + /// [initialRoute] properties must have their default values, as they will have no effect. /// /// The `supportedLocales` argument must be a list of one or more elements. /// By default supportedLocales is `[const Locale('en', 'US')]`. + /// + /// {@tool dartpad} + /// This sample shows a basic Flutter application using [WidgetsApp]. + /// + /// ** See code in examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart ** + /// {@end-tool} WidgetsApp({ // can't be const because the asserts use methods on Iterable :-( super.key, this.navigatorKey, @@ -530,8 +536,23 @@ class WidgetsApp extends StatefulWidget { /// The [PageRoute] generator callback used when the app is navigated to a /// named route. /// + /// A [PageRoute] represents the page in a [Navigator], so that it can + /// correctly animate between pages, and to represent the "return value" of + /// a route (e.g. which button a user selected in a modal dialog). + /// /// This callback can be used, for example, to specify that a [MaterialPageRoute] /// or a [CupertinoPageRoute] should be used for building page transitions. + /// + /// The [PageRouteFactory] type is generic, meaning the provided function must + /// itself be generic. For example (with special emphasis on the `` at the + /// start of the closure): + /// + /// ```dart + /// pageRouteBuilder: (RouteSettings settings, WidgetBuilder builder) => PageRouteBuilder( + /// settings: settings, + /// pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) => builder(context), + /// ), + /// ``` final PageRouteFactory? pageRouteBuilder; /// {@template flutter.widgets.widgetsApp.routeInformationParser} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index e59cd13b01..b64315bd91 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -5657,9 +5657,17 @@ class RichText extends MultiChildRenderObjectWidget { final ui.TextHeightBehavior? textHeightBehavior; /// The [SelectionRegistrar] this rich text is subscribed to. + /// + /// If this is set, [selectionColor] must be non-null. final SelectionRegistrar? selectionRegistrar; /// The color to use when painting the selection. + /// + /// This is ignored if [selectionRegistrar] is null. + /// + /// See the section on selections in the [RichText] top-level API + /// documentation for more details on enabling selection in [RichText] + /// widgets. final Color? selectionColor; @override diff --git a/packages/flutter/lib/src/widgets/debug.dart b/packages/flutter/lib/src/widgets/debug.dart index b70c5b624f..9e346c1a08 100644 --- a/packages/flutter/lib/src/widgets/debug.dart +++ b/packages/flutter/lib/src/widgets/debug.dart @@ -252,6 +252,10 @@ bool debugItemsHaveDuplicateKeys(Iterable items) { /// assert(debugCheckHasTable(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This method can be expensive (it walks the element tree). /// /// Does nothing if asserts are disabled. Always returns true. @@ -282,6 +286,10 @@ bool debugCheckHasTable(BuildContext context) { /// assert(debugCheckHasMediaQuery(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// Does nothing if asserts are disabled. Always returns true. bool debugCheckHasMediaQuery(BuildContext context) { assert(() { @@ -334,6 +342,10 @@ bool debugCheckHasMediaQuery(BuildContext context) { /// If they are non-null, they are included in the order above, interspersed /// with the more generic advice regarding [Directionality]. /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// Does nothing if asserts are disabled. Always returns true. bool debugCheckHasDirectionality(BuildContext context, { String? why, String? hint, String? alternative }) { assert(() { @@ -406,6 +418,10 @@ void debugWidgetBuilderValue(Widget widget, Widget? built) { /// assert(debugCheckHasWidgetsLocalizations(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// Does nothing if asserts are disabled. Always returns true. bool debugCheckHasWidgetsLocalizations(BuildContext context) { assert(() { @@ -443,6 +459,10 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) { /// assert(debugCheckHasOverlay(context)); /// ``` /// +/// Always place this before any early returns, so that the invariant is checked +/// in all cases. This prevents bugs from hiding until a particular codepath is +/// hit. +/// /// This method can be expensive (it walks the element tree). /// /// Does nothing if asserts are disabled. Always returns true. diff --git a/packages/flutter/lib/src/widgets/default_selection_style.dart b/packages/flutter/lib/src/widgets/default_selection_style.dart index 26c480b7c6..5a9ad93718 100644 --- a/packages/flutter/lib/src/widgets/default_selection_style.dart +++ b/packages/flutter/lib/src/widgets/default_selection_style.dart @@ -31,7 +31,7 @@ class DefaultSelectionStyle extends InheritedTheme { }); /// A const-constructable default selection style that provides fallback - /// values. + /// values (null). /// /// Returned from [of] when the given [BuildContext] doesn't have an enclosing /// default selection style. @@ -43,6 +43,12 @@ class DefaultSelectionStyle extends InheritedTheme { selectionColor = null, super(child: const _NullWidget()); + /// The default cursor and selection color (semi-transparent grey). + /// + /// This is the color that the [Text] widget uses when the specified selection + /// color is null. + static const Color defaultColor = Color(0x80808080); + /// The color of the text field's cursor. /// /// The cursor indicates the current location of the text insertion point in @@ -88,9 +94,9 @@ class _NullWidget extends StatelessWidget { @override Widget build(BuildContext context) { throw FlutterError( - 'A DefaultTextStyle constructed with DefaultTextStyle.fallback cannot be incorporated into the widget tree, ' - 'it is meant only to provide a fallback value returned by DefaultTextStyle.of() ' - 'when no enclosing default text style is present in a BuildContext.', + 'A DefaultSelectionStyle constructed with DefaultSelectionStyle.fallback cannot be incorporated into the widget tree, ' + 'it is meant only to provide a fallback value returned by DefaultSelectionStyle.of() ' + 'when no enclosing default selection style is present in a BuildContext.', ); } } diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 10793d0ab1..067f76320b 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -572,7 +572,7 @@ class EditableText extends StatefulWidget { /// /// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus], /// [showSelectionHandles], [enableInteractiveSelection], [forceLine], - /// [style], [cursorColor], [cursorOpacityAnimates],[backgroundCursorColor], + /// [style], [cursorColor], [cursorOpacityAnimates], [backgroundCursorColor], /// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle], /// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding], /// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer], diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 380a6f934e..3d98d8c00d 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -281,6 +281,10 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { /// navigation and being able to insert widgets on top of the pages in an app. /// To simply display a stack of widgets, consider using [Stack] instead. /// +/// An [Overlay] widget requires a [Directionality] widget to be in scope, so +/// that it can resolve direction-sensitive coordinates of any +/// [Positioned.directional] children. +/// /// {@tool dartpad} /// This example shows how to use the [Overlay] to highlight the [NavigationBar] /// destination. diff --git a/packages/flutter/lib/src/widgets/pages.dart b/packages/flutter/lib/src/widgets/pages.dart index 46bb9f2644..4fe48202ce 100644 --- a/packages/flutter/lib/src/widgets/pages.dart +++ b/packages/flutter/lib/src/widgets/pages.dart @@ -7,6 +7,13 @@ import 'framework.dart'; import 'routes.dart'; /// A modal route that replaces the entire screen. +/// +/// The [PageRouteBuilder] subclass provides a way to create a [PageRoute] using +/// callbacks rather than by defining a new class via subclassing. +/// +/// See also: +/// +/// * [Route], which documents the meaning of the `T` generic type argument. abstract class PageRoute extends ModalRoute { /// Creates a modal route that replaces the entire screen. PageRoute({ @@ -49,6 +56,13 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation animat /// /// Callers must define the [pageBuilder] function which creates the route's /// primary contents. To add transitions define the [transitionsBuilder] function. +/// +/// The `T` generic type argument corresponds to the type argument of the +/// created [Route] objects. +/// +/// See also: +/// +/// * [Route], which documents the meaning of the `T` generic type argument. class PageRouteBuilder extends PageRoute { /// Creates a route that delegates to builder callbacks. /// @@ -86,6 +100,8 @@ class PageRouteBuilder extends PageRoute { /// /// See [ModalRoute.buildTransitions] for complete definition of the parameters. /// {@endtemplate} + /// + /// The default transition is a jump cut (i.e. no animation). final RouteTransitionsBuilder transitionsBuilder; @override diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 116a347a80..5dbeadebbb 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -34,6 +34,10 @@ import 'transitions.dart'; // late StateSetter setState; /// A route that displays widgets in the [Navigator]'s [Overlay]. +/// +/// See also: +/// +/// * [Route], which documents the meaning of the `T` generic type argument. abstract class OverlayRoute extends Route { /// Creates a route that knows how to interact with an [Overlay]. OverlayRoute({ @@ -85,6 +89,10 @@ abstract class OverlayRoute extends Route { } /// A route with entrance and exit transitions. +/// +/// See also: +/// +/// * [Route], which documents the meaning of the `T` generic type argument. abstract class TransitionRoute extends OverlayRoute { /// Creates a route that animates itself when it is pushed or popped. TransitionRoute({ @@ -507,6 +515,10 @@ class LocalHistoryEntry { /// pop internally if its list of local history entries is non-empty. Rather /// than being removed as the current route, the most recent [LocalHistoryEntry] /// is removed from the list and its [LocalHistoryEntry.onRemove] is called. +/// +/// See also: +/// +/// * [Route], which documents the meaning of the `T` generic type argument. mixin LocalHistoryRoute on Route { List? _localHistory; int _entriesImpliesAppBarDismissal = 0; @@ -946,6 +958,10 @@ class _ModalScopeState extends State<_ModalScope> { /// /// The `T` type argument is the return value of the route. If there is no /// return value, consider using `void` as the return value. +/// +/// See also: +/// +/// * [Route], which further documents the meaning of the `T` generic type argument. abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute { /// Creates a route that blocks interaction with previous routes. ModalRoute({ diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart index 2aa7624a6f..58bfad0df0 100644 --- a/packages/flutter/lib/src/widgets/selectable_region.dart +++ b/packages/flutter/lib/src/widgets/selectable_region.dart @@ -26,6 +26,10 @@ import 'selection_container.dart'; import 'text_editing_intents.dart'; import 'text_selection.dart'; +// Examples can assume: +// FocusNode _focusNode = FocusNode(); +// late GlobalKey key; + const Set _kLongPressSelectionDevices = { PointerDeviceKind.touch, PointerDeviceKind.stylus, @@ -34,13 +38,22 @@ const Set _kLongPressSelectionDevices = { /// A widget that introduces an area for user selections. /// -/// Flutter widgets are not selectable by default. To enable selection for -/// a Flutter application, consider wrapping a portion of widget subtree with -/// [SelectableRegion]. The wrapped subtree can be selected by users using mouse -/// or touch gestures, e.g. users can select widgets by holding the mouse +/// Flutter widgets are not selectable by default. Wrapping a widget subtree +/// with a [SelectableRegion] widget enables selection within that subtree (for +/// example, [Text] widgets automatically look for selectable regions to enable +/// selection). The wrapped subtree can be selected by users using mouse or +/// touch gestures, e.g. users can select widgets by holding the mouse /// left-click and dragging across widgets, or they can use long press gestures /// to select words on touch devices. /// +/// A [SelectableRegion] widget requires configuration; in particular specific +/// [selectionControls] must be provided. +/// +/// The [SelectionArea] widget from the [material] library configures a +/// [SelectableRegion] in a platform-specific manner (e.g. using a Material +/// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be +/// simpler to use that widget rather than using [SelectableRegion] directly. +/// /// ## An overview of the selection system. /// /// Every [Selectable] under the [SelectableRegion] can be selected. They form a @@ -77,7 +90,7 @@ const Set _kLongPressSelectionDevices = { /// MaterialApp( /// home: SelectableRegion( /// selectionControls: materialTextSelectionControls, -/// focusNode: FocusNode(), +/// focusNode: _focusNode, // initialized to FocusNode() /// child: Scaffold( /// appBar: AppBar(title: const Text('Flutter Code Sample')), /// body: ListView( @@ -160,6 +173,16 @@ const Set _kLongPressSelectionDevices = { /// child selection area can not extend past its subtree, and the selection of /// the parent selection area can not extend inside the child selection area. /// +/// ## Tests +/// +/// In a test, a region can be selected either by faking drag events (e.g. using +/// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region +/// that has been given a [GlobalKey], e.g.: +/// +/// ```dart +/// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard)); +/// ``` +/// /// See also: /// * [SelectionArea], which creates a [SelectableRegion] with /// platform-adaptive selection controls. @@ -202,6 +225,9 @@ class SelectableRegion extends StatefulWidget { /// The delegate to build the selection handles and toolbar for mobile /// devices. + /// + /// The [emptyTextSelectionControls] global variable provides a default + /// [TextSelectionControls] implementation with no controls. final TextSelectionControls selectionControls; @override diff --git a/packages/flutter/lib/src/widgets/selection_container.dart b/packages/flutter/lib/src/widgets/selection_container.dart index 1f4687d987..df3070958d 100644 --- a/packages/flutter/lib/src/widgets/selection_container.dart +++ b/packages/flutter/lib/src/widgets/selection_container.dart @@ -14,7 +14,8 @@ import 'framework.dart'; /// /// The state of this container is a single selectable and will register /// itself to the [registrar] if provided. Otherwise, it will register to the -/// [SelectionRegistrar] from the context. +/// [SelectionRegistrar] from the context. Consider using a [SelectionArea] +/// widget to provide a root registrar. /// /// The containers handle the [SelectionEvent]s from the registered /// [SelectionRegistrar] and delegate the events to the [delegate]. diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart index e656f01139..ba0edc15af 100644 --- a/packages/flutter/lib/src/widgets/text.dart +++ b/packages/flutter/lib/src/widgets/text.dart @@ -533,6 +533,13 @@ class Text extends StatelessWidget { final ui.TextHeightBehavior? textHeightBehavior; /// The color to use when painting the selection. + /// + /// This is ignored if [SelectionContainer.maybeOf] returns null + /// in the [BuildContext] of the [Text] widget. + /// + /// If null, the ambient [DefaultSelectionStyle] is used (if any); failing + /// that, the selection color defaults to [DefaultSelectionStyle.defaultColor] + /// (semi-transparent grey). final Color? selectionColor; @override @@ -558,7 +565,7 @@ class Text extends StatelessWidget { textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.of(context), selectionRegistrar: registrar, - selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor, + selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor, text: TextSpan( style: effectiveTextStyle, text: data, diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index cbf555723c..81f0515e6b 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -26,6 +26,7 @@ import 'tap_region.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; +export 'package:flutter/rendering.dart' show TextSelectionPoint; export 'package:flutter/services.dart' show TextSelectionDelegate; /// A duration that controls how often the drag selection update callback is @@ -76,6 +77,11 @@ class ToolbarItemsParentData extends ContainerBoxParentData { /// implementer of the toolbar widget. /// /// Override text operations such as [handleCut] if needed. +/// +/// See also: +/// +/// * [SelectionArea], which selects appropriate text selection controls +/// based on the current platform. abstract class TextSelectionControls { /// Builds a selection handle of the given `type`. /// @@ -97,20 +103,20 @@ abstract class TextSelectionControls { /// /// Typically displays buttons for copying and pasting text. /// - /// [globalEditableRegion] is the TextField size of the global coordinate system - /// in logical pixels. + /// The [globalEditableRegion] parameter is the TextField size of the global + /// coordinate system in logical pixels. /// - /// [textLineHeight] is the `preferredLineHeight` of the [RenderEditable] we - /// are building a toolbar for. + /// The [textLineHeight] parameter is the [RenderEditable.preferredLineHeight] + /// of the [RenderEditable] we are building a toolbar for. /// - /// The [position] is a general calculation midpoint parameter of the toolbar. - /// If you want more detailed position information, can use [endpoints] - /// to calculate it. + /// The [selectionMidpoint] parameter is a general calculation midpoint + /// parameter of the toolbar. More detailed position information + /// is computable from the [endpoints] parameter. Widget buildToolbar( BuildContext context, Rect globalEditableRegion, double textLineHeight, - Offset position, + Offset selectionMidpoint, List endpoints, TextSelectionDelegate delegate, ValueListenable? clipboardStatus, @@ -207,6 +213,55 @@ abstract class TextSelectionControls { } } +/// Text selection controls that do not show any toolbars or handles. +/// +/// This is a placeholder, suitable for temporary use during development, but +/// not practical for production. For example, it provides no way for the user +/// to interact with selections: no context menus on desktop, no toolbars or +/// drag handles on mobile, etc. For production, consider using +/// [MaterialTextSelectionControls] or creating a custom subclass of +/// [TextSelectionControls]. +/// +/// The [emptyTextSelectionControls] global variable has a +/// suitable instance of this class. +class EmptyTextSelectionControls extends TextSelectionControls { + @override + Size getHandleSize(double textLineHeight) => Size.zero; + + @override + Widget buildToolbar( + BuildContext context, + Rect globalEditableRegion, + double textLineHeight, + Offset selectionMidpoint, + List endpoints, + TextSelectionDelegate delegate, + ValueListenable? clipboardStatus, + Offset? lastSecondaryTapDownPosition, + ) => const SizedBox.shrink(); + + @override + Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) { + return const SizedBox.shrink(); + } + + @override + Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) { + return Offset.zero; + } +} + +/// Text selection controls that do not show any toolbars or handles. +/// +/// This is a placeholder, suitable for temporary use during development, but +/// not practical for production. For example, it provides no way for the user +/// to interact with selections: no context menus on desktop, no toolbars or +/// drag handles on mobile, etc. For production, consider using +/// [materialTextSelectionControls] or creating a custom subclass of +/// [TextSelectionControls]. +final TextSelectionControls emptyTextSelectionControls = EmptyTextSelectionControls(); + + /// An object that manages a pair of text selection handles for a /// [RenderEditable]. /// diff --git a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart index 7403999b96..a64a694b6a 100644 --- a/packages/flutter/test/cupertino/text_selection_toolbar_test.dart +++ b/packages/flutter/test/cupertino/text_selection_toolbar_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; diff --git a/packages/flutter/test/material/text_selection_toolbar_test.dart b/packages/flutter/test/material/text_selection_toolbar_test.dart index e6d8c1d3c9..6e6b9ec88d 100644 --- a/packages/flutter/test/material/text_selection_toolbar_test.dart +++ b/packages/flutter/test/material/text_selection_toolbar_test.dart @@ -4,7 +4,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; diff --git a/packages/flutter/test/widgets/default_colors_test.dart b/packages/flutter/test/widgets/default_colors_test.dart new file mode 100644 index 0000000000..14a965195c --- /dev/null +++ b/packages/flutter/test/widgets/default_colors_test.dart @@ -0,0 +1,153 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const double _crispText = 100.0; // this font size is selected to avoid needing any antialiasing. +const String _expText = 'Éxp'; // renders in Ahem as: + +// ######## +// ######## +// ######## +// ######## +// +// ÉÉÉÉxxxxpppp + +void main() { + testWidgets('Default background', (WidgetTester tester) async { + await tester.pumpWidget(const Align( + alignment: Alignment.topLeft, + child: Text(_expText, textDirection: TextDirection.ltr, style: TextStyle(color: Color(0xFF345678), fontSize: _crispText))), + ); + await _expectColors( + tester, + find.byType(Align), + { const Color(0x00000000), const Color(0xFF345678) }, + { + Offset.zero: const Color(0xFF345678), // the text + const Offset(10, 10): const Color(0xFF345678), // the text + const Offset(50, 95): const Color(0x00000000), // the background (under the É) + const Offset(250, 50): const Color(0x00000000), // the text (above the p) + const Offset(250, 95): const Color(0xFF345678), // the text (the p) + const Offset(400, 400): const Color(0x00000000), // the background + const Offset(799, 599): const Color(0x00000000), // the background + }, + ); + }, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently. + + testWidgets('Default text color', (WidgetTester tester) async { + await tester.pumpWidget(const ColoredBox( + color: Color(0xFFABCDEF), + child: Align( + alignment: Alignment.topLeft, + child: Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText)), + ), + ), + ); + await _expectColors( + tester, + find.byType(Align), + { const Color(0xFFABCDEF), const Color(0xFFFFFFFF) }, + { + Offset.zero: const Color(0xFFFFFFFF), // the text + const Offset(10, 10): const Color(0xFFFFFFFF), // the text + const Offset(50, 95): const Color(0xFFABCDEF), // the background (under the É) + const Offset(250, 50): const Color(0xFFABCDEF), // the text (above the p) + const Offset(250, 95): const Color(0xFFFFFFFF), // the text (the p) + const Offset(400, 400): const Color(0xFFABCDEF), // the background + const Offset(799, 599): const Color(0xFFABCDEF), // the background + }, + ); + }, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently. + + testWidgets('Default text selection color', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + ColoredBox( + color: const Color(0xFFFFFFFF), + child: Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: Overlay( + initialEntries: [ + OverlayEntry( + builder: (BuildContext context) => SelectableRegion( + focusNode: FocusNode(), + selectionControls: emptyTextSelectionControls, + child: Align( + key: key, + alignment: Alignment.topLeft, + child: const Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText)), + ), + ), + ), + ], + ), + ), + ), + ), + ); + await _expectColors( + tester, + find.byType(Align), + { const Color(0xFFFFFFFF) }, + ); + // fake a "select all" event to selecte the text + Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard)); + await tester.pump(); + await _expectColors( + tester, + find.byType(Align), + { const Color(0xFFFFFFFF), const Color(0xFFBFBFBF) }, // 0x80808080 blended with 0xFFFFFFFF + { + Offset.zero: const Color(0xFFBFBFBF), // the selected text + const Offset(10, 10): const Color(0xFFBFBFBF), // the selected text + const Offset(50, 95): const Color(0xFFBFBFBF), // the selected background (under the É) + const Offset(250, 50): const Color(0xFFBFBFBF), // the selected background (above the p) + const Offset(250, 95): const Color(0xFFBFBFBF), // the selected text (the p) + const Offset(400, 400): const Color(0xFFFFFFFF), // the background + const Offset(799, 599): const Color(0xFFFFFFFF), // the background + }, + ); + }, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently. +} + +Color _getPixel(ByteData bytes, int x, int y, int width) { + final int offset = (x + y * width) * 4; + return Color.fromARGB( + bytes.getUint8(offset + 3), + bytes.getUint8(offset + 0), + bytes.getUint8(offset + 1), + bytes.getUint8(offset + 2), + ); +} + +Future _expectColors(WidgetTester tester, Finder finder, Set allowedColors, [ Map? spotChecks ]) async { + final TestWidgetsFlutterBinding binding = tester.binding; + final ui.Image image = (await binding.runAsync(() => captureImage(finder.evaluate().single)))!; + final ByteData bytes = (await binding.runAsync(() => image.toByteData(format: ui.ImageByteFormat.rawStraightRgba)))!; + final Set actualColorValues = {}; + for (int offset = 0; offset < bytes.lengthInBytes; offset += 4) { + actualColorValues.add((bytes.getUint8(offset + 3) << 24) + + (bytes.getUint8(offset + 0) << 16) + + (bytes.getUint8(offset + 1) << 8) + + (bytes.getUint8(offset + 2))); + } + final Set actualColors = actualColorValues.map((int value) => Color(value)).toSet(); + expect(actualColors, allowedColors); + spotChecks?.forEach((Offset position, Color expected) { + assert(position.dx.round() >= 0); + assert(position.dx.round() < image.width); + assert(position.dy.round() >= 0); + assert(position.dy.round() < image.height); + final Offset precisePosition = position * binding.window.devicePixelRatio; + final Color actual = _getPixel(bytes, precisePosition.dx.round(), precisePosition.dy.round(), image.width); + expect(actual, expected, reason: 'Pixel at $position is $actual but expected $expected.'); + }); +} diff --git a/packages/flutter_test/lib/flutter_test.dart b/packages/flutter_test/lib/flutter_test.dart index d7531fff1a..2767c589a3 100644 --- a/packages/flutter_test/lib/flutter_test.dart +++ b/packages/flutter_test/lib/flutter_test.dart @@ -56,6 +56,7 @@ library flutter_test; export 'dart:async' show Future; export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; +export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart'; export 'src/accessibility.dart'; export 'src/all_elements.dart'; export 'src/animation_sheet.dart'; diff --git a/packages/flutter_test/lib/src/_matchers_io.dart b/packages/flutter_test/lib/src/_matchers_io.dart index 7c15279d77..702fdfa83a 100644 --- a/packages/flutter_test/lib/src/_matchers_io.dart +++ b/packages/flutter_test/lib/src/_matchers_io.dart @@ -30,6 +30,15 @@ Future captureImage(Element element) { return layer.toImage(renderObject.paintBounds); } +/// Whether or not [captureImage] is supported. +/// +/// This can be used to skip tests on platforms that don't support +/// capturing images. +/// +/// Currently this is true except when tests are running in the context of a web +/// browser (`flutter test --platform chrome`). +const bool canCaptureImage = true; + /// The matcher created by [matchesGoldenFile]. This class is enabled when the /// test is running on a VM using conditional import. class MatchesGoldenFile extends AsyncMatcher { @@ -84,7 +93,7 @@ class MatchesGoldenFile extends AsyncMatcher { throw AssertionError('must provide a Finder, Image, Future, List, or Future>'); } - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); + final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; return binding.runAsync(() async { final ui.Image? image = await imageFuture; if (image == null) { diff --git a/packages/flutter_test/lib/src/_matchers_web.dart b/packages/flutter_test/lib/src/_matchers_web.dart index d2d769eafa..1fe0323ac5 100644 --- a/packages/flutter_test/lib/src/_matchers_web.dart +++ b/packages/flutter_test/lib/src/_matchers_web.dart @@ -18,6 +18,15 @@ Future captureImage(Element element) { throw UnsupportedError('captureImage is not supported on the web.'); } +/// Whether or not [captureImage] is supported. +/// +/// This can be used to skip tests on platforms that don't support +/// capturing images. +/// +/// Currently this is true except when tests are running in the context of a web +/// browser (`flutter test --platform chrome`). +const bool canCaptureImage = false; + /// The matcher created by [matchesGoldenFile]. This class is enabled when the /// test is running in a web browser using conditional import. class MatchesGoldenFile extends AsyncMatcher { @@ -47,7 +56,7 @@ class MatchesGoldenFile extends AsyncMatcher { final Element element = elements.single; final RenderObject renderObject = _findRepaintBoundary(element); final Size size = renderObject.paintBounds.size; - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); + final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; final Element e = binding.renderViewElement!; // Unlike `flutter_tester`, we don't have the ability to render an element diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 3245f9acff..a20d6fce24 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -1131,28 +1131,45 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { addTime(additionalTime); - return realAsyncZone.run>(() async { + return realAsyncZone.run>(() { + final Completer result = Completer(); _pendingAsyncTasks = Completer(); - T? result; try { - result = await callback(); + callback().then(result.complete).catchError( + (Object exception, StackTrace stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'Flutter test framework', + context: ErrorDescription('while running async test code'), + informationCollector: () { + return [ + ErrorHint('The exception was caught asynchronously.'), + ]; + }, + )); + result.complete(null); + }, + ); } catch (exception, stack) { FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'Flutter test framework', context: ErrorDescription('while running async test code'), + informationCollector: () { + return [ + ErrorHint('The exception was caught synchronously.'), + ]; + }, )); - } finally { - // We complete the _pendingAsyncTasks future successfully regardless of - // whether an exception occurred because in the case of an exception, - // we already reported the exception to FlutterError. Moreover, - // completing the future with an error would trigger an unhandled - // exception due to zone error boundaries. + result.complete(null); + } + result.future.whenComplete(() { _pendingAsyncTasks!.complete(); _pendingAsyncTasks = null; - } - return result; + }); + return result.future; }); } diff --git a/packages/flutter_test/lib/src/buffer_matcher.dart b/packages/flutter_test/lib/src/buffer_matcher.dart index 875fa0cefa..5701b605a3 100644 --- a/packages/flutter_test/lib/src/buffer_matcher.dart +++ b/packages/flutter_test/lib/src/buffer_matcher.dart @@ -60,11 +60,11 @@ class _BufferGoldenMatcher extends AsyncMatcher { /// golden files. This parameter is optional. /// /// {@tool snippet} -/// Sample invocations of [matchesGoldenFile]. +/// Sample invocations of [bufferMatchesGoldenFile]. /// /// ```dart /// await expectLater( -/// const [], +/// const [ /* bytes... */ ], /// bufferMatchesGoldenFile('sample.png'), /// ); /// ``` diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 0d069761ae..cad66d3e00 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -2030,7 +2030,7 @@ class _MatchesReferenceImage extends AsyncMatcher { imageFuture = captureImage(elements.single); } - final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); + final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; return binding.runAsync(() async { final ui.Image image = await imageFuture; final ByteData? bytes = await image.toByteData(); diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 3eddb29e72..bad6ceef70 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -630,6 +630,23 @@ void main() { key: 'abczed', }); }); + + testWidgets('control test (return value)', (WidgetTester tester) async { + final String? result = await tester.binding.runAsync(() async => 'Judy Turner'); + expect(result, 'Judy Turner'); + }); + + testWidgets('async throw', (WidgetTester tester) async { + final String? result = await tester.binding.runAsync(() async => throw Exception('Lois Dilettente')); + expect(result, isNull); + expect(tester.takeException(), isNotNull); + }); + + testWidgets('sync throw', (WidgetTester tester) async { + final String? result = await tester.binding.runAsync(() => throw Exception('Butch Barton')); + expect(result, isNull); + expect(tester.takeException(), isNotNull); + }); }); group('showKeyboard', () {