diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index ea1b496c1743..15bddf1f10a2 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -19,6 +19,7 @@ import 'framework.dart'; import 'platform_menu_bar.dart'; import 'router.dart'; import 'service_extensions.dart'; +import 'view.dart'; import 'widget_inspector.dart'; export 'dart:ui' show AppLifecycleState, Locale; @@ -896,6 +897,22 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB @override bool get framesEnabled => super.framesEnabled && _readyToProduceFrames; + /// Used by [runApp] to wrap the provided `rootWidget` in the default [View]. + /// + /// The [View] determines into what [FlutterView] the app is rendered into. + /// For backwards-compatibility reasons, this method currently chooses + /// [window] (which is a [FlutterView]) as the rendering target. This will + /// change in a future version of Flutter. + /// + /// The `rootWidget` widget provided to this method must not already be + /// wrapped in a [View]. + Widget wrapWithDefaultView(Widget rootWidget) { + return View( + view: window, + child: rootWidget, + ); + } + /// Schedules a [Timer] for attaching the root widget. /// /// This is called by [runApp] to configure the widget tree. Consider using @@ -1014,8 +1031,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to /// ensure the widget, element, and render trees are all built. void runApp(Widget app) { - WidgetsFlutterBinding.ensureInitialized() - ..scheduleAttachRootWidget(app) + final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized(); + binding + ..scheduleAttachRootWidget(binding.wrapWithDefaultView(app)) ..scheduleWarmUpFrame(); } diff --git a/packages/flutter/lib/src/widgets/view.dart b/packages/flutter/lib/src/widgets/view.dart new file mode 100644 index 000000000000..0a2068599fa3 --- /dev/null +++ b/packages/flutter/lib/src/widgets/view.dart @@ -0,0 +1,94 @@ +// 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:ui' show FlutterView; + +import 'framework.dart'; +import 'lookup_boundary.dart'; + +/// Injects a [FlutterView] into the tree and makes it available to descendants +/// within the same [LookupBoundary] via [View.of] and [View.maybeOf]. +/// +/// In a future version of Flutter, the functionality of this widget will be +/// extended to actually bootstrap the render tree that is going to be rendered +/// into the provided [view]. This will enable rendering content into multiple +/// [FlutterView]s from a single widget tree. +/// +/// Each [FlutterView] can be associated with at most one [View] widget in the +/// widget tree. Two or more [View] widgets configured with the same +/// [FlutterView] must never exist within the same widget tree at the same time. +/// Internally, this limitation is enforced by a [GlobalObjectKey] that derives +/// its identity from the [view] provided to this widget. +class View extends InheritedWidget { + /// Injects the provided [view] into the widget tree. + View({required this.view, required super.child}) : super(key: GlobalObjectKey(view)); + + /// The [FlutterView] to be injected into the tree. + final FlutterView view; + + @override + bool updateShouldNotify(View oldWidget) => view != oldWidget.view; + + /// Returns the [FlutterView] that the provided `context` will render into. + /// + /// Returns null if the `context` is not associated with a [FlutterView]. + /// + /// The method creates a dependency on the `context`, which will be informed + /// when the identity of the [FlutterView] changes (i.e. the `context` is + /// moved to render into a different [FlutterView] then before). The context + /// will not be informed when the properties on the [FlutterView] itself + /// change their values. To access the property values of a [FlutterView] it + /// is best practise to use [MediaQuery.maybeOf] instead, which will ensure + /// that the `context` is informed when the view properties change. + /// + /// See also: + /// + /// * [View.of], which throws instead of returning null if no [FlutterView] + /// is found. + static FlutterView? maybeOf(BuildContext context) { + return LookupBoundary.dependOnInheritedWidgetOfExactType(context)?.view; + } + + /// Returns the [FlutterView] that the provided `context` will render into. + /// + /// Throws if the `context` is not associated with a [FlutterView]. + /// + /// The method creates a dependency on the `context`, which will be informed + /// when the identity of the [FlutterView] changes (i.e. the `context` is + /// moved to render into a different [FlutterView] then before). The context + /// will not be informed when the properties on the [FlutterView] itself + /// change their values. To access the property values of a [FlutterView] it + /// is best practise to use [MediaQuery.of] instead, which will ensure that + /// the `context` is informed when the view properties change. + /// + /// See also: + /// + /// * [View.maybeOf], which throws instead of returning null if no + /// [FlutterView] is found. + static FlutterView of(BuildContext context) { + final FlutterView? result = maybeOf(context); + assert(() { + if (result == null) { + final bool hiddenByBoundary = LookupBoundary.debugIsHidingAncestorWidgetOfExactType(context); + final List information = [ + if (hiddenByBoundary) ...[ + ErrorSummary('View.of() was called with a context that does not have access to a View widget.'), + ErrorDescription('The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.'), + ] else ...[ + ErrorSummary('View.of() was called with a context that does not contain a View widget.'), + ErrorDescription('No View widget ancestor could be found starting from the context that was passed to View.of().'), + ], + ErrorDescription( + 'The context used was:\n' + ' $context', + ), + ErrorHint('This usually means that the provided context is not associated with a View.'), + ]; + throw FlutterError.fromParts(information); + } + return true; + }()); + return result!; + } +} diff --git a/packages/flutter/lib/src/widgets/window.dart b/packages/flutter/lib/src/widgets/window.dart new file mode 100644 index 000000000000..873df8ff4543 --- /dev/null +++ b/packages/flutter/lib/src/widgets/window.dart @@ -0,0 +1,8 @@ +// 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. + +/// Placeholder to be used in a future version of Flutter. +abstract class Window { + const Window._(); +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index e2c5e39b10e5..8c0b48dffa34 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -148,8 +148,11 @@ export 'src/widgets/transitions.dart'; export 'src/widgets/tween_animation_builder.dart'; export 'src/widgets/unique_widget.dart'; export 'src/widgets/value_listenable_builder.dart'; +// TODO(goderbauer): Enable once clean-up in google3 is done. +// export 'src/widgets/view.dart'; export 'src/widgets/viewport.dart'; export 'src/widgets/visibility.dart'; export 'src/widgets/widget_inspector.dart'; export 'src/widgets/widget_span.dart'; export 'src/widgets/will_pop_scope.dart'; +export 'src/widgets/window.dart'; diff --git a/packages/flutter/test/material/debug_test.dart b/packages/flutter/test/material/debug_test.dart index ce7b4dec45ba..8c49223c6593 100644 --- a/packages/flutter/test/material/debug_test.dart +++ b/packages/flutter/test/material/debug_test.dart @@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('debugCheckHasMaterial control test', (WidgetTester tester) async { - await tester.pumpWidget(const Chip(label: Text('label'))); + await tester.pumpWidget(const Center(child: Chip(label: Text('label')))); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); final FlutterError error = exception as FlutterError; @@ -25,7 +25,7 @@ void main() { expect(error.diagnostics[3], isA>()); expect(error.diagnostics[4], isA()); expect( - error.toStringDeep(), + error.toStringDeep(), startsWith( 'FlutterError\n' ' No Material widget found.\n' ' Chip widgets require a Material widget ancestor within the\n' @@ -42,12 +42,13 @@ void main() { ' The specific widget that could not find a Material ancestor was:\n' ' Chip\n' ' The ancestors of this widget were:\n' - ' [root]\n', - ); + ' Center\n' + // End of ancestor chain omitted, not relevant for test. + )); }); testWidgets('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async { - await tester.pumpWidget(const BackButton()); + await tester.pumpWidget(const Center(child: BackButton())); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); final FlutterError error = exception as FlutterError; @@ -64,7 +65,7 @@ void main() { expect(error.diagnostics[4], isA>()); expect(error.diagnostics[5], isA()); expect( - error.toStringDeep(), + error.toStringDeep(), startsWith( 'FlutterError\n' ' No MaterialLocalizations found.\n' ' BackButton widgets require MaterialLocalizations to be provided\n' @@ -78,8 +79,9 @@ void main() { ' ancestor was:\n' ' BackButton\n' ' The ancestors of this widget were:\n' - ' [root]\n', - ); + ' Center\n' + // End of ancestor chain omitted, not relevant for test. + )); }); testWidgets('debugCheckHasScaffold control test', (WidgetTester tester) async { @@ -233,6 +235,7 @@ void main() { ' HeroControllerScope\n' ' ScrollConfiguration\n' ' MaterialApp\n' + ' View-[GlobalObjectKey TestWindow#00000]\n' ' [root]\n' ' Typically, the Scaffold widget is introduced by the MaterialApp\n' ' or WidgetsApp widget at the top of your application widget tree.\n' @@ -377,6 +380,7 @@ void main() { ' Scaffold-[LabeledGlobalKey#00000]\n' ' MediaQuery\n' ' Directionality\n' + ' View-[GlobalObjectKey TestWindow#00000]\n' ' [root]\n' ' Typically, the ScaffoldMessenger widget is introduced by the\n' ' MaterialApp at the top of your application widget tree.\n' diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index c0a7187c277a..893009d15092 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -2458,6 +2458,7 @@ void main() { ' Scaffold\n' ' MediaQuery\n' ' Directionality\n' + ' View-[GlobalObjectKey TestWindow#e6136]\n' ' [root]\n' ' Typically, the ScaffoldMessenger widget is introduced by the\n' ' MaterialApp at the top of your application widget tree.\n', diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 944e444d1186..585d5e6ad324 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -7390,7 +7390,6 @@ void main() { final dynamic exception = tester.takeException(); expect(exception, isFlutterError); expect(exception.toString(), startsWith('No Material widget found.')); - expect(exception.toString(), endsWith(':\n $textField\nThe ancestors of this widget were:\n [root]')); }); testWidgets('TextField loses focus when disabled', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/container_test.dart b/packages/flutter/test/widgets/container_test.dart index f322a4d0a0f7..f519a381fe8f 100644 --- a/packages/flutter/test/widgets/container_test.dart +++ b/packages/flutter/test/widgets/container_test.dart @@ -122,14 +122,16 @@ void main() { box.toStringDeep(), equalsIgnoringHashCodes( 'RenderPadding#00000 relayoutBoundary=up1\n' - ' │ creator: Padding ← Container ← Align ← [root]\n' + ' │ creator: Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ size: Size(63.0, 88.0)\n' ' │ padding: EdgeInsets.all(5.0)\n' ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' - ' │ creator: ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -137,7 +139,7 @@ void main() { ' │\n' ' └─child: RenderDecoratedBox#00000\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← [root]\n' + ' │ Align ← View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -149,7 +151,8 @@ void main() { ' │\n' ' └─child: _RenderColoredBox#00000\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' - ' │ Container ← Align ← [root]\n' + ' │ Container ← Align ← View-[GlobalObjectKey TestWindow#00000] ←\n' + ' │ [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -157,7 +160,8 @@ void main() { ' │\n' ' └─child: RenderPadding#00000\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' - ' │ Padding ← Container ← Align ← [root]\n' + ' │ Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ size: Size(53.0, 78.0)\n' @@ -165,7 +169,8 @@ void main() { ' │\n' ' └─child: RenderPositionedBox#00000\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ size: Size(39.0, 64.0)\n' @@ -175,7 +180,8 @@ void main() { ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ size: Size(25.0, 33.0)\n' @@ -184,7 +190,7 @@ void main() { ' └─child: RenderDecoratedBox#00000\n' ' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n' ' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n' - ' [root]\n' + ' View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' parentData: (can use size)\n' ' constraints: BoxConstraints(w=25.0, h=33.0)\n' ' size: Size(25.0, 33.0)\n' @@ -192,7 +198,7 @@ void main() { ' color: Color(0xffffff00)\n' ' configuration: ImageConfiguration(bundle:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 1.0, platform:\n' - ' android)\n', + ' android)\n' ), ); @@ -200,7 +206,8 @@ void main() { box.toStringDeep(minLevel: DiagnosticLevel.fine), equalsIgnoringHashCodes( 'RenderPadding#00000 relayoutBoundary=up1\n' - ' │ creator: Padding ← Container ← Align ← [root]\n' + ' │ creator: Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ layer: null\n' @@ -210,7 +217,8 @@ void main() { ' │ textDirection: null\n' ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' - ' │ creator: ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ layer: null\n' @@ -220,7 +228,7 @@ void main() { ' │\n' ' └─child: RenderDecoratedBox#00000\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← [root]\n' + ' │ Align ← View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -240,7 +248,8 @@ void main() { ' │\n' ' └─child: _RenderColoredBox#00000\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' - ' │ Container ← Align ← [root]\n' + ' │ Container ← Align ← View-[GlobalObjectKey TestWindow#00000] ←\n' + ' │ [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -250,7 +259,8 @@ void main() { ' │\n' ' └─child: RenderPadding#00000\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' - ' │ Padding ← Container ← Align ← [root]\n' + ' │ Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -261,7 +271,8 @@ void main() { ' │\n' ' └─child: RenderPositionedBox#00000\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ layer: null\n' @@ -274,7 +285,8 @@ void main() { ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ layer: null\n' @@ -285,7 +297,7 @@ void main() { ' └─child: RenderDecoratedBox#00000\n' ' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n' ' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n' - ' [root]\n' + ' View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' parentData: (can use size)\n' ' constraints: BoxConstraints(w=25.0, h=33.0)\n' ' layer: null\n' @@ -301,7 +313,7 @@ void main() { ' shape: rectangle\n' ' configuration: ImageConfiguration(bundle:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 1.0, platform:\n' - ' android)\n', + ' android)\n' ), ); @@ -310,7 +322,8 @@ void main() { equalsIgnoringHashCodes( 'RenderPadding#00000 relayoutBoundary=up1\n' ' │ needsCompositing: false\n' - ' │ creator: Padding ← Container ← Align ← [root]\n' + ' │ creator: Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ layer: null\n' @@ -323,7 +336,8 @@ void main() { ' │\n' ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ needsCompositing: false\n' - ' │ creator: ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ creator: ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(5.0, 5.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=790.0, 0.0<=h<=590.0)\n' ' │ layer: null\n' @@ -336,7 +350,7 @@ void main() { ' └─child: RenderDecoratedBox#00000\n' ' │ needsCompositing: false\n' ' │ creator: DecoratedBox ← ConstrainedBox ← Padding ← Container ←\n' - ' │ Align ← [root]\n' + ' │ Align ← View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -359,7 +373,8 @@ void main() { ' └─child: _RenderColoredBox#00000\n' ' │ needsCompositing: false\n' ' │ creator: ColoredBox ← DecoratedBox ← ConstrainedBox ← Padding ←\n' - ' │ Container ← Align ← [root]\n' + ' │ Container ← Align ← View-[GlobalObjectKey TestWindow#00000] ←\n' + ' │ [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -372,7 +387,8 @@ void main() { ' └─child: RenderPadding#00000\n' ' │ needsCompositing: false\n' ' │ creator: Padding ← ColoredBox ← DecoratedBox ← ConstrainedBox ←\n' - ' │ Padding ← Container ← Align ← [root]\n' + ' │ Padding ← Container ← Align ← View-[GlobalObjectKey\n' + ' │ TestWindow#00000] ← [root]\n' ' │ parentData: (can use size)\n' ' │ constraints: BoxConstraints(w=53.0, h=78.0)\n' ' │ layer: null\n' @@ -386,7 +402,8 @@ void main() { ' └─child: RenderPositionedBox#00000\n' ' │ needsCompositing: false\n' ' │ creator: Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(7.0, 7.0) (can use size)\n' ' │ constraints: BoxConstraints(w=39.0, h=64.0)\n' ' │ layer: null\n' @@ -402,7 +419,8 @@ void main() { ' └─child: RenderConstrainedBox#00000 relayoutBoundary=up1\n' ' │ needsCompositing: false\n' ' │ creator: SizedBox ← Align ← Padding ← ColoredBox ← DecoratedBox ←\n' - ' │ ConstrainedBox ← Padding ← Container ← Align ← [root]\n' + ' │ ConstrainedBox ← Padding ← Container ← Align ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │ parentData: offset=Offset(14.0, 31.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=39.0, 0.0<=h<=64.0)\n' ' │ layer: null\n' @@ -416,7 +434,7 @@ void main() { ' needsCompositing: false\n' ' creator: DecoratedBox ← SizedBox ← Align ← Padding ← ColoredBox ←\n' ' DecoratedBox ← ConstrainedBox ← Padding ← Container ← Align ←\n' - ' [root]\n' + ' View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' parentData: (can use size)\n' ' constraints: BoxConstraints(w=25.0, h=33.0)\n' ' layer: null\n' @@ -434,7 +452,7 @@ void main() { ' shape: rectangle\n' ' configuration: ImageConfiguration(bundle:\n' ' PlatformAssetBundle#00000(), devicePixelRatio: 1.0, platform:\n' - ' android)\n', + ' android)\n' ), ); diff --git a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart index 2af7c7553334..43f6d394b678 100644 --- a/packages/flutter/test/widgets/custom_multi_child_layout_test.dart +++ b/packages/flutter/test/widgets/custom_multi_child_layout_test.dart @@ -372,7 +372,8 @@ void main() { ' in its parent data.\n' ' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n' ' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n' - ' CustomMultiChildLayout ← Center ← [root]\n' + ' CustomMultiChildLayout ← Center ← View-[GlobalObjectKey\n' + ' TestWindow#00000] ← [root]\n' ' parentData: offset=Offset(0.0, 0.0); id=null\n' ' constraints: MISSING\n' ' size: MISSING\n' diff --git a/packages/flutter/test/widgets/debug_test.dart b/packages/flutter/test/widgets/debug_test.dart index 15da4c42d256..476e420bce42 100644 --- a/packages/flutter/test/widgets/debug_test.dart +++ b/packages/flutter/test/widgets/debug_test.dart @@ -79,14 +79,13 @@ void main() { expect(error.diagnostics[2], isA>()); expect( error.toStringDeep(), - equalsIgnoringHashCodes( + startsWith( 'FlutterError\n' ' No Table widget found.\n' ' Builder widgets require a Table widget ancestor.\n' ' The specific widget that could not find a Table ancestor was:\n' ' Builder\n' - ' The ownership chain for the affected widget is: "Builder ←\n' - ' [root]"\n', + ' The ownership chain for the affected widget is: "Builder ←', // End of ownership chain omitted, not relevant for test. ), ); } @@ -122,15 +121,20 @@ void main() { ); expect( error.toStringDeep(), - equalsIgnoringHashCodes( + startsWith( 'FlutterError\n' ' No MediaQuery widget ancestor found.\n' ' Builder widgets require a MediaQuery widget ancestor.\n' ' The specific widget that could not find a MediaQuery ancestor\n' ' was:\n' ' Builder\n' - ' The ownership chain for the affected widget is: "Builder ←\n' - ' [root]"\n' + ' The ownership chain for the affected widget is: "Builder ←' // Full chain omitted, not relevant for test. + ), + ); + expect( + error.toStringDeep(), + endsWith( + '[root]"\n' // End of ownership chain. ' No MediaQuery ancestor could be found starting from the context\n' ' that was passed to MediaQuery.of(). This can happen because you\n' ' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n' diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart index 08ef7b87755b..dd36f86a0ee8 100644 --- a/packages/flutter/test/widgets/focus_manager_test.dart +++ b/packages/flutter/test/widgets/focus_manager_test.dart @@ -1228,7 +1228,8 @@ void main() { equalsIgnoringHashCodes( 'FocusManager#00000\n' ' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\n' - ' │ primaryFocusCreator: Container-[GlobalKey#00000] ← [root]\n' + ' │ primaryFocusCreator: Container-[GlobalKey#00000] ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' ' │\n' ' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n' ' │ IN FOCUS PATH\n' @@ -1266,7 +1267,6 @@ void main() { }); }); - group('Autofocus', () { testWidgets( 'works when the previous focused node is detached', @@ -1695,7 +1695,6 @@ void main() { debugPrint = oldDebugPrint; } final String messagesStr = messages.toString(); - expect(messagesStr.split('\n').length, equals(58)); expect(messagesStr, contains(RegExp(r' └─Child 1: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)'))); expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes')); expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1'))); diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 6bcae11dbfd5..6d698d3ef8d3 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -1183,13 +1183,13 @@ void main() { expect(exception, isFlutterError); expect( exception.toString(), - equalsIgnoringHashCodes( + startsWith( 'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n' 'This typically means that the `_EmptyWidget` or its children\n' 'are not a subtype of `RenderObjectWidget`.\n' 'The following element does not have an associated render object:\n' ' _EmptyWidget\n' - 'debugCreator: _EmptyWidget ← Column ← [root]', + 'debugCreator: _EmptyWidget ← Column ← ', // Omitted end of debugCreator chain because it's irrelevant for test. ), ); }); @@ -1216,13 +1216,13 @@ void main() { expect(exception, isFlutterError); expect( exception.toString(), - equalsIgnoringHashCodes( + startsWith( 'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n' 'This typically means that the `_EmptyWidget` or its children\n' 'are not a subtype of `RenderObjectWidget`.\n' 'The following element does not have an associated render object:\n' ' _EmptyWidget\n' - 'debugCreator: _EmptyWidget ← Column ← [root]', + 'debugCreator: _EmptyWidget ← Column ← ', // Omitted end of debugCreator chain because it's irrelevant for test. ), ); }); diff --git a/packages/flutter/test/widgets/init_state_test.dart b/packages/flutter/test/widgets/init_state_test.dart index 331536736a18..828893db68c9 100644 --- a/packages/flutter/test/widgets/init_state_test.dart +++ b/packages/flutter/test/widgets/init_state_test.dart @@ -30,7 +30,7 @@ class TestWidgetState extends State { void main() { testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async { await tester.pumpWidget(const Parent(child: TestWidget())); - expect(ancestors, equals(['Parent', 'RenderObjectToWidgetAdapter'])); + expect(ancestors, equals(['Parent', 'View', 'RenderObjectToWidgetAdapter'])); }); } diff --git a/packages/flutter/test/widgets/media_query_test.dart b/packages/flutter/test/widgets/media_query_test.dart index 9be4d6522387..0dea1093ca78 100644 --- a/packages/flutter/test/widgets/media_query_test.dart +++ b/packages/flutter/test/widgets/media_query_test.dart @@ -63,15 +63,20 @@ void main() { expect(error.diagnostics.last, isA()); expect( error.toStringDeep(), - equalsIgnoringHashCodes( + startsWith( 'FlutterError\n' ' No MediaQuery widget ancestor found.\n' ' Builder widgets require a MediaQuery widget ancestor.\n' ' The specific widget that could not find a MediaQuery ancestor\n' ' was:\n' ' Builder\n' - ' The ownership chain for the affected widget is: "Builder ←\n' - ' [root]"\n' + ' The ownership chain for the affected widget is: "Builder ←', // Full ownership chain omitted, not relevant for test. + ), + ); + expect( + error.toStringDeep(), + endsWith( + '[root]"\n' // End of ownership chain. ' No MediaQuery ancestor could be found starting from the context\n' ' that was passed to MediaQuery.of(). This can happen because you\n' ' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n' diff --git a/packages/flutter/test/widgets/parent_data_test.dart b/packages/flutter/test/widgets/parent_data_test.dart index 4ddeada094c7..c416352816e0 100644 --- a/packages/flutter/test/widgets/parent_data_test.dart +++ b/packages/flutter/test/widgets/parent_data_test.dart @@ -274,7 +274,7 @@ void main() { expect(exception, isFlutterError); expect( exception.toString(), - equalsIgnoringHashCodes( + startsWith( 'Incorrect use of ParentDataWidget.\n' 'The following ParentDataWidgets are providing parent data to the same RenderObject:\n' '- Positioned(left: 7.0, top: 6.0) (typically placed directly inside a Stack widget)\n' @@ -283,7 +283,7 @@ void main() { 'Usually, this indicates that at least one of the offending ParentDataWidgets listed ' 'above is not placed directly inside a compatible ancestor widget.\n' 'The ownership chain for the RenderObject that received the parent data was:\n' - ' DecoratedBox ← Positioned ← Positioned ← Stack ← Directionality ← [root]', + ' DecoratedBox ← Positioned ← Positioned ← Stack ← Directionality ← ', // End of chain omitted, not relevant for test. ), ); @@ -311,7 +311,7 @@ void main() { expect(exception, isFlutterError); expect( exception.toString(), - equalsIgnoringHashCodes( + startsWith( 'Incorrect use of ParentDataWidget.\n' 'The ParentDataWidget Positioned(left: 7.0, top: 6.0) wants to apply ParentData of type ' 'StackParentData to a RenderObject, which has been set up to accept ParentData of ' @@ -320,7 +320,7 @@ void main() { 'Typically, Positioned widgets are placed directly inside Stack widgets.\n' 'The offending Positioned is currently placed inside a Row widget.\n' 'The ownership chain for the RenderObject that received the incompatible parent data was:\n' - ' DecoratedBox ← Positioned ← Row ← DummyWidget ← Directionality ← [root]', + ' DecoratedBox ← Positioned ← Row ← DummyWidget ← Directionality ← ', // End of chain omitted, not relevant for test. ), ); @@ -410,7 +410,7 @@ void main() { expect(exception, isFlutterError); expect( exception.toString(), - equalsIgnoringHashCodes( + startsWith( 'Incorrect use of ParentDataWidget.\n' 'The ParentDataWidget Expanded(flex: 1) wants to apply ParentData of type ' 'FlexParentData to a RenderObject, which has been set up to accept ParentData of ' @@ -419,7 +419,7 @@ void main() { 'Typically, Expanded widgets are placed directly inside Flex widgets.\n' 'The offending Expanded is currently placed inside a Stack widget.\n' 'The ownership chain for the RenderObject that received the incompatible parent data was:\n' - ' LimitedBox ← Container ← Expanded ← Stack ← Row ← Directionality ← [root]', + ' LimitedBox ← Container ← Expanded ← Stack ← Row ← Directionality ← ', // Omitted end of debugCreator chain because it's irrelevant for test. ), ); }); diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index ae7bce637b4d..900b4d8b448d 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -2486,7 +2486,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', '_PlatformViewPlaceHolder']), + containsAllInOrder(['PlatformViewLink', '_PlatformViewPlaceHolder']), ); onPlatformViewCreatedCallBack(createdPlatformViewId); @@ -2495,7 +2495,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), + containsAllInOrder(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), ); expect(createdPlatformViewId, currentViewId + 1); @@ -2535,7 +2535,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['Center', 'SizedBox', 'PlatformViewLink', '_PlatformViewPlaceHolder']), + containsAllInOrder(['Center', 'SizedBox', 'PlatformViewLink', '_PlatformViewPlaceHolder']), ); // 'create' should not have been called by PlatformViewLink, since its @@ -2580,7 +2580,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', '_PlatformViewPlaceHolder']), + containsAllInOrder(['PlatformViewLink', '_PlatformViewPlaceHolder']), ); // Layout should have triggered a create call. Simulate the callback @@ -2592,7 +2592,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), + containsAllInOrder(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), ); expect(createdPlatformViewId, currentViewId + 1); @@ -2678,7 +2678,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', '_PlatformViewPlaceHolder']), + containsAllInOrder(['PlatformViewLink', '_PlatformViewPlaceHolder']), ); onPlatformViewCreatedCallBack(createdPlatformViewId); @@ -2687,7 +2687,7 @@ void main() { expect( tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), - equals(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), + containsAllInOrder(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), ); expect(createdPlatformViewId, currentViewId + 1); diff --git a/packages/flutter/test/widgets/slotted_render_object_widget_test.dart b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart index 5e48a3ad6894..257951a86eb6 100644 --- a/packages/flutter/test/widgets/slotted_render_object_widget_test.dart +++ b/packages/flutter/test/widgets/slotted_render_object_widget_test.dart @@ -219,27 +219,30 @@ void main() { expect( tester.renderObject(find.byType(_Diagonal)).toStringDeep(), - equalsIgnoringHashCodes(r''' -_RenderDiagonal#00000 relayoutBoundary=up1 - │ creator: _Diagonal ← Align ← Directionality ← [root] - │ parentData: offset=Offset(0.0, 0.0) (can use size) - │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0) - │ size: Size(190.0, 220.0) - │ - ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2 - │ creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] - │ parentData: offset=Offset(0.0, 0.0) (can use size) - │ constraints: BoxConstraints(unconstrained) - │ size: Size(80.0, 100.0) - │ additionalConstraints: BoxConstraints(w=80.0, h=100.0) - │ - └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2 - creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] - parentData: offset=Offset(80.0, 100.0) (can use size) - constraints: BoxConstraints(unconstrained) - size: Size(110.0, 120.0) - additionalConstraints: BoxConstraints(w=110.0, h=120.0) -''') + equalsIgnoringHashCodes( + '_RenderDiagonal#00000 relayoutBoundary=up1\n' + ' │ creator: _Diagonal ← Align ← Directionality ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' + ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' + ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' + ' │ size: Size(190.0, 220.0)\n' + ' │\n' + ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n' + ' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' + ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n' + ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' + ' │ constraints: BoxConstraints(unconstrained)\n' + ' │ size: Size(80.0, 100.0)\n' + ' │ additionalConstraints: BoxConstraints(w=80.0, h=100.0)\n' + ' │\n' + ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n' + ' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' + ' View-[GlobalObjectKey TestWindow#00000] ← [root]\n' + ' parentData: offset=Offset(80.0, 100.0) (can use size)\n' + ' constraints: BoxConstraints(unconstrained)\n' + ' size: Size(110.0, 120.0)\n' + ' additionalConstraints: BoxConstraints(w=110.0, h=120.0)\n', + ) ); }); } diff --git a/packages/flutter/test/widgets/stack_test.dart b/packages/flutter/test/widgets/stack_test.dart index ea5c4909f421..d65d4b132fce 100644 --- a/packages/flutter/test/widgets/stack_test.dart +++ b/packages/flutter/test/widgets/stack_test.dart @@ -783,21 +783,27 @@ void main() { await tester.pumpWidget( Stack(), ); + final String exception = tester.takeException().toString(); + expect( - tester.takeException().toString(), + exception, startsWith( 'No Directionality widget found.\n' "Stack widgets require a Directionality widget ancestor to resolve the 'alignment' argument.\n" "The default value for 'alignment' is AlignmentDirectional.topStart, which requires a text direction.\n" 'The specific widget that could not find a Directionality ancestor was:\n' ' Stack\n' - 'The ownership chain for the affected widget is: "Stack ← [root]"\n' + 'The ownership chain for the affected widget is: "Stack ← ', // Omitted full ownership chain because it is not relevant for the test. + )); + expect( + exception, endsWith( + '← [root]"\n' // End of ownership chain. 'Typically, the Directionality widget is introduced by the MaterialApp or WidgetsApp widget at the ' 'top of your application widget tree. It determines the ambient reading direction and is used, for ' 'example, to determine how to lay out text, how to interpret "start" and "end" values, and to resolve ' 'EdgeInsetsDirectional, AlignmentDirectional, and other *Directional objects.\n' 'Instead of providing a Directionality widget, another solution would be passing a non-directional ' "'alignment', or an explicit 'textDirection', to the Stack.", - ); + )); }); testWidgets('Can update clipBehavior of IndexedStack', diff --git a/packages/flutter/test/widgets/view_test.dart b/packages/flutter/test/widgets/view_test.dart new file mode 100644 index 000000000000..695a69dd9608 --- /dev/null +++ b/packages/flutter/test/widgets/view_test.dart @@ -0,0 +1,71 @@ +// 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:ui'; + +import 'package:flutter/src/widgets/view.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Widgets running with runApp can find View', (WidgetTester tester) async { + FlutterView? viewOf; + FlutterView? viewMaybeOf; + + runApp( + Builder( + builder: (BuildContext context) { + viewOf = View.of(context); + viewMaybeOf = View.maybeOf(context); + return Container(); + }, + ), + ); + + expect(viewOf, isNotNull); + expect(viewOf, isA()); + expect(viewMaybeOf, isNotNull); + expect(viewMaybeOf, isA()); + }); + + testWidgets('Widgets running with pumpWidget can find View', (WidgetTester tester) async { + FlutterView? view; + FlutterView? viewMaybeOf; + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + view = View.of(context); + viewMaybeOf = View.maybeOf(context); + return Container(); + }, + ), + ); + + expect(view, isNotNull); + expect(view, isA()); + expect(viewMaybeOf, isNotNull); + expect(viewMaybeOf, isA()); + }); + + testWidgets('cannot find View behind a LookupBoundary', (WidgetTester tester) async { + await tester.pumpWidget( + LookupBoundary( + child: Container(), + ), + ); + + final BuildContext context = tester.element(find.byType(Container)); + + expect(View.maybeOf(context), isNull); + expect( + () => View.of(context), + throwsA(isA().having( + (FlutterError error) => error.message, + 'message', + contains('The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.'), + )), + ); + }); +} diff --git a/packages/flutter/test/widgets/widget_inspector_test.dart b/packages/flutter/test/widgets/widget_inspector_test.dart index df876e4aeb19..e213f71a1bd9 100644 --- a/packages/flutter/test/widgets/widget_inspector_test.dart +++ b/packages/flutter/test/widgets/widget_inspector_test.dart @@ -4277,11 +4277,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { final Element element = tester.element(find.byType(Directionality).first); Element? root; element.visitAncestorElements((Element ancestor) { - if (root == null) { - root = ancestor; - // Stop traversing ancestors. - return false; - } + root = ancestor; return true; }); expect(root, isNotNull); diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 2040f0ce629e..d78669a5a95b 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -553,12 +553,24 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker EnginePhase phase = EnginePhase.sendSemanticsUpdate, ]) { return TestAsyncUtils.guard(() { - binding.attachRootWidget(widget); - binding.scheduleFrame(); - return binding.pump(duration, phase); + return _pumpWidget( + binding.wrapWithDefaultView(widget), + duration, + phase, + ); }); } + Future _pumpWidget( + Widget widget, [ + Duration? duration, + EnginePhase phase = EnginePhase.sendSemanticsUpdate, + ]) { + binding.attachRootWidget(widget); + binding.scheduleFrame(); + return binding.pump(duration, phase); + } + @override Future> handlePointerEventRecord(Iterable records) { assert(records != null); @@ -697,7 +709,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker // The interval following the last frame doesn't have to be within the fullDuration. Duration elapsed = Duration.zero; return TestAsyncUtils.guard(() async { - binding.attachRootWidget(target); + binding.attachRootWidget(binding.wrapWithDefaultView(target)); binding.scheduleFrame(); while (elapsed < maxDuration) { await binding.pump(interval); @@ -720,12 +732,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker 'therefore no restoration data has been collected to restore from. Did you forget to wrap ' 'your widget tree in a RootRestorationScope?', ); - final Widget widget = ((binding.renderViewElement! as RenderObjectToWidgetElement).widget as RenderObjectToWidgetAdapter).child!; - final TestRestorationData restorationData = binding.restorationManager.restorationData; - runApp(Container(key: UniqueKey())); - await pump(); - binding.restorationManager.restoreFrom(restorationData); - return pumpWidget(widget); + return TestAsyncUtils.guard(() async { + final Widget widget = ((binding.renderViewElement! as RenderObjectToWidgetElement).widget as RenderObjectToWidgetAdapter).child!; + final TestRestorationData restorationData = binding.restorationManager.restorationData; + runApp(Container(key: UniqueKey())); + await pump(); + binding.restorationManager.restoreFrom(restorationData); + return _pumpWidget(widget); + }); } /// Retrieves the current restoration data from the [RestorationManager].