Unverified Commit 671d8eaf authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Relands "Add runWidget to bootstrap a widget tree without a default View" (#142344)

Reverts flutter/flutter#142339

In the original change one of the tests included the same view twice which resulted in a different failure than the expected one. The second commit contains the fix for this. I don't understand how this wasn't caught presubmit on CI.
parent a5ad088f
...@@ -24,6 +24,10 @@ import 'widget_inspector.dart'; ...@@ -24,6 +24,10 @@ import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
// Examples can assume:
// late FlutterView myFlutterView;
// class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => const Placeholder(); }
/// Interface for classes that register with the Widgets layer binding. /// Interface for classes that register with the Widgets layer binding.
/// ///
/// This can be used by any class, not just widgets. It provides an interface /// This can be used by any class, not just widgets. It provides an interface
...@@ -1152,14 +1156,20 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -1152,14 +1156,20 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
} }
} }
/// Inflate the given widget and attach it to the screen. /// Inflate the given widget and attach it to the view.
///
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
/// The [runApp] method renders the provided `app` widget into the
/// [PlatformDispatcher.implicitView] by wrapping it in a [View] widget, which
/// will bootstrap the render tree for the app. Apps that want to control which
/// [FlutterView] they render into can use [runWidget] instead.
/// ///
/// The widget is given constraints during layout that force it to fill the /// The widget is given constraints during layout that force it to fill the
/// entire screen. If you wish to align your widget to one side of the screen /// entire view. If you wish to align your widget to one side of the view
/// (e.g., the top), consider using the [Align] widget. If you wish to center /// (e.g., the top), consider using the [Align] widget. If you wish to center
/// your widget, you can also use the [Center] widget. /// your widget, you can also use the [Center] widget.
/// ///
/// Calling [runApp] again will detach the previous root widget from the screen /// Calling [runApp] again will detach the previous root widget from the view
/// and attach the given widget in its place. The new widget tree is compared /// and attach the given widget in its place. The new widget tree is compared
/// against the previous widget tree and any differences are applied to the /// against the previous widget tree and any differences are applied to the
/// underlying render tree, similar to what happens when a [StatefulWidget] /// underlying render tree, similar to what happens when a [StatefulWidget]
...@@ -1167,6 +1177,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -1167,6 +1177,7 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// ///
/// Initializes the binding using [WidgetsFlutterBinding] if necessary. /// Initializes the binding using [WidgetsFlutterBinding] if necessary.
/// ///
/// {@template flutter.widgets.runApp.shutdown}
/// ## Application shutdown /// ## Application shutdown
/// ///
/// This widget tree is not torn down when the application shuts down, because /// This widget tree is not torn down when the application shuts down, because
...@@ -1178,29 +1189,32 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -1178,29 +1189,32 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// Applications are responsible for ensuring that they are well-behaved /// Applications are responsible for ensuring that they are well-behaved
/// even in the face of a rapid unscheduled termination. /// even in the face of a rapid unscheduled termination.
/// ///
/// To artificially cause the entire widget tree to be disposed, consider
/// calling [runApp] with a widget such as [SizedBox.shrink].
///
/// To listen for platform shutdown messages (and other lifecycle changes), /// To listen for platform shutdown messages (and other lifecycle changes),
/// consider the [AppLifecycleListener] API. /// consider the [AppLifecycleListener] API.
/// {@endtemplate}
/// ///
/// ## Dismissing Flutter UI via platform native methods /// To artificially cause the entire widget tree to be disposed, consider
/// calling [runApp] with a widget such as [SizedBox.shrink].
/// ///
/// {@template flutter.widgets.runApp.dismissal} /// {@template flutter.widgets.runApp.dismissal}
/// ## Dismissing Flutter UI via platform native methods
///
/// An application may have both Flutter and non-Flutter UI in it. If the /// An application may have both Flutter and non-Flutter UI in it. If the
/// application calls non-Flutter methods to remove Flutter based UI such as /// application calls non-Flutter methods to remove Flutter based UI such as
/// platform native API to manipulate the platform native navigation stack, /// platform native API to manipulate the platform native navigation stack,
/// the framework does not know if the developer intends to eagerly free /// the framework does not know if the developer intends to eagerly free
/// resources or not. The widget tree remains mounted and ready to render /// resources or not. The widget tree remains mounted and ready to render
/// as soon as it is displayed again. /// as soon as it is displayed again.
/// {@endtemplate}
/// ///
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/) /// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
/// and use it to call [runApp] with a widget such as [SizedBox.shrink] when /// and use it to call [runApp] with a widget such as [SizedBox.shrink] when
/// the framework should dispose of the active widget tree. /// the framework should dispose of the active widget tree.
/// {@endtemplate}
/// ///
/// See also: /// See also:
/// ///
/// * [runWidget], which bootstraps a widget tree without assuming the
/// [FlutterView] into which it will be rendered.
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the /// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
/// widget hierarchy. /// widget hierarchy.
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root /// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
...@@ -1209,9 +1223,75 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -1209,9 +1223,75 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// ensure the widget, element, and render trees are all built. /// ensure the widget, element, and render trees are all built.
void runApp(Widget app) { void runApp(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized(); final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
assert(binding.debugCheckZone('runApp')); _runWidget(binding.wrapWithDefaultView(app), binding, 'runApp');
}
/// Inflate the given widget and bootstrap the widget tree.
///
// TODO(goderbauer): Update the paragraph below to include the Window widget once that exists.
/// Unlike [runApp], this method does not define a [FlutterView] into which the
/// provided `app` widget is rendered into. It is up to the caller to include at
/// least one [View] widget in the provided `app` widget that will bootstrap a
/// render tree and define the [FlutterView] into which content is rendered.
/// [RenderObjectWidget]s without an ancestor [View] widget will result in an
/// exception. Apps that want to render into the default view without dealing
/// with view management should consider calling [runApp] instead.
///
/// {@tool snippet}
/// The sample shows how to utilize [runWidget] to specify the [FlutterView]
/// into which the `MyApp` widget will be drawn:
///
/// ```dart
/// runWidget(
/// View(
/// view: myFlutterView,
/// child: const MyApp(),
/// ),
/// );
/// ```
/// {@end-tool}
///
/// Calling [runWidget] again will detach the previous root widget and attach
/// the given widget in its place. The new widget tree is compared against the
/// previous widget tree and any differences are applied to the underlying
/// render tree, similar to what happens when a [StatefulWidget] rebuilds after
/// calling [State.setState].
///
/// Initializes the binding using [WidgetsFlutterBinding] if necessary.
///
/// {@macro flutter.widgets.runApp.shutdown}
///
/// To artificially cause the entire widget tree to be disposed, consider
/// calling [runWidget] with a [ViewCollection] that does not specify any
/// [ViewCollection.views].
///
/// ## Dismissing Flutter UI via platform native methods
///
/// {@macro flutter.widgets.runApp.dismissal}
///
/// To release resources more eagerly, establish a [platform channel](https://flutter.dev/platform-channels/)
/// and use it to remove the [View] whose widget resources should be released
/// from the `app` widget tree provided to [runWidget].
///
/// See also:
///
/// * [runApp], which bootstraps a widget tree and renders it into a default
/// [FlutterView].
/// * [WidgetsBinding.attachRootWidget], which creates the root widget for the
/// widget hierarchy.
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which creates the root
/// element for the element hierarchy.
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
/// ensure the widget, element, and render trees are all built.
void runWidget(Widget app) {
final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
_runWidget(app, binding, 'runWidget');
}
void _runWidget(Widget app, WidgetsBinding binding, String debugEntryPoint) {
assert(binding.debugCheckZone(debugEntryPoint));
binding binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app)) ..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame(); ..scheduleWarmUpFrame();
} }
......
...@@ -1325,10 +1325,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable { ...@@ -1325,10 +1325,11 @@ abstract class State<T extends StatefulWidget> with Diagnosticable {
/// To listen for platform shutdown messages (and other lifecycle changes), /// To listen for platform shutdown messages (and other lifecycle changes),
/// consider the [AppLifecycleListener] API. /// consider the [AppLifecycleListener] API.
/// ///
/// ### Dismissing Flutter UI via platform native methods
///
/// {@macro flutter.widgets.runApp.dismissal} /// {@macro flutter.widgets.runApp.dismissal}
/// ///
/// See the method used to bootstrap the app (e.g. [runApp] or [runWidget])
/// for suggestions on how to release resources more eagerly.
///
/// See also: /// See also:
/// ///
/// * [deactivate], which is called prior to [dispose]. /// * [deactivate], which is called prior to [dispose].
...@@ -6970,9 +6971,16 @@ abstract class RenderTreeRootElement extends RenderObjectElement { ...@@ -6970,9 +6971,16 @@ abstract class RenderTreeRootElement extends RenderObjectElement {
'however, expects that a child will be attached.', 'however, expects that a child will be attached.',
), ),
ErrorHint( ErrorHint(
'Try moving the subtree that contains the ${toStringShort()} widget into the ' 'Try moving the subtree that contains the ${toStringShort()} widget '
'view property of a ViewAnchor widget or to the root of the widget tree, where ' 'to a location where it is not expected to attach its RenderObject '
'it is not expected to attach its RenderObject to a parent.', 'to a parent. This could mean moving the subtree into the view '
'property of a "ViewAnchor" widget or - if the subtree is the root of '
'your widget tree - passing it to "runWidget" instead of "runApp".',
),
ErrorHint(
'If you are seeing this error in a test and the subtree containing '
'the ${toStringShort()} widget is passed to "WidgetTester.pumpWidget", '
'consider setting the "wrapWithView" parameter of that method to false.'
), ),
], ],
); );
......
...@@ -13,9 +13,9 @@ void main() { ...@@ -13,9 +13,9 @@ void main() {
..physicalConstraints = ViewConstraints.tight(const Size(1008.0, 2198.0)) ..physicalConstraints = ViewConstraints.tight(const Size(1008.0, 2198.0))
..devicePixelRatio = 1.912500023841858; ..devicePixelRatio = 1.912500023841858;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: View( View(
view: view, view: view,
child: const SizedBox(), child: const SizedBox(),
), ),
...@@ -35,9 +35,3 @@ class FlutterViewSpy extends TestFlutterView { ...@@ -35,9 +35,3 @@ class FlutterViewSpy extends TestFlutterView {
sizes.add(size); sizes.add(size);
} }
} }
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
...@@ -98,9 +98,9 @@ void main() { ...@@ -98,9 +98,9 @@ void main() {
testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async { testWidgets('debugCheckHasMediaQuery control test', (WidgetTester tester) async {
// Cannot use tester.pumpWidget here because it wraps the widget in a View, // Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor. // which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
late FlutterError error; late FlutterError error;
try { try {
...@@ -343,9 +343,3 @@ void main() { ...@@ -343,9 +343,3 @@ void main() {
expect(renderObject.debugLayer?.debugCreator, isNotNull); expect(renderObject.debugLayer?.debugCreator, isNotNull);
}); });
} }
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
...@@ -47,9 +47,9 @@ void main() { ...@@ -47,9 +47,9 @@ void main() {
late final FlutterError error; late final FlutterError error;
// Cannot use tester.pumpWidget here because it wraps the widget in a View, // Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor. // which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
try { try {
MediaQuery.of(context); MediaQuery.of(context);
...@@ -111,9 +111,9 @@ void main() { ...@@ -111,9 +111,9 @@ void main() {
bool tested = false; bool tested = false;
// Cannot use tester.pumpWidget here because it wraps the widget in a View, // Cannot use tester.pumpWidget here because it wraps the widget in a View,
// which introduces a MediaQuery ancestor. // which introduces a MediaQuery ancestor.
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
final MediaQueryData? data = MediaQuery.maybeOf(context); final MediaQueryData? data = MediaQuery.maybeOf(context);
expect(data, isNull); expect(data, isNull);
...@@ -287,9 +287,9 @@ void main() { ...@@ -287,9 +287,9 @@ void main() {
late MediaQueryData data; late MediaQueryData data;
MediaQueryData? outerData; MediaQueryData? outerData;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
outerData = MediaQuery.maybeOf(context); outerData = MediaQuery.maybeOf(context);
return MediaQuery.fromView( return MediaQuery.fromView(
...@@ -342,9 +342,9 @@ void main() { ...@@ -342,9 +342,9 @@ void main() {
late MediaQueryData data; late MediaQueryData data;
MediaQueryData? outerData; MediaQueryData? outerData;
int rebuildCount = 0; int rebuildCount = 0;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
outerData = MediaQuery.maybeOf(context); outerData = MediaQuery.maybeOf(context);
return MediaQuery.fromView( return MediaQuery.fromView(
...@@ -1531,9 +1531,3 @@ void main() { ...@@ -1531,9 +1531,3 @@ void main() {
] ]
)); ));
} }
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
...@@ -26,7 +26,7 @@ void main() { ...@@ -26,7 +26,7 @@ void main() {
final RootWidget rootWidget = RootWidget( final RootWidget rootWidget = RootWidget(
child: View( child: View(
view: tester.view, view: FakeFlutterView(tester.view),
child: const ColoredBox(color: Colors.orange), child: const ColoredBox(color: Colors.orange),
), ),
); );
...@@ -36,4 +36,52 @@ void main() { ...@@ -36,4 +36,52 @@ void main() {
expect(tester.binding.rootElement!.widget, equals(rootWidget)); expect(tester.binding.rootElement!.widget, equals(rootWidget));
expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner)); expect(tester.element(find.byType(ColoredBox)).owner, equals(tester.binding.buildOwner));
}); });
testWidgets('runApp throws if given a View', (WidgetTester tester) async {
runApp(
View(
view: FakeFlutterView(tester.view),
child: const SizedBox.shrink(),
),
);
expect(
tester.takeException(),
isFlutterError.having(
(FlutterError e) => e.message,
'message',
contains('passing it to "runWidget" instead of "runApp"'),
),
);
});
testWidgets('runWidget throws if not given a View', (WidgetTester tester) async {
runWidget(const SizedBox.shrink());
expect(
tester.takeException(),
isFlutterError.having(
(FlutterError e) => e.message,
'message',
contains('Try wrapping your widget in a View widget'),
),
);
});
testWidgets('runWidget does not throw if given a View', (WidgetTester tester) async {
runWidget(
View(
view: FakeFlutterView(tester.view),
child: const SizedBox.shrink(),
),
);
expect(find.byType(View), findsOne);
});
testWidgets('can call runWidget with an empty ViewCollection', (WidgetTester tester) async {
runWidget(const ViewCollection(views: <Widget>[]));
expect(find.byType(ViewCollection), findsOne);
});
}
class FakeFlutterView extends TestFlutterView {
FakeFlutterView(TestFlutterView view) : super(view: view, display: view.display, platformDispatcher: view.platformDispatcher);
} }
...@@ -15,9 +15,9 @@ void main() { ...@@ -15,9 +15,9 @@ void main() {
child: const TestWidget(), child: const TestWidget(),
); );
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: widget, widget,
); );
expect(find.text('Hello'), findsOneWidget); expect(find.text('Hello'), findsOneWidget);
...@@ -29,9 +29,9 @@ void main() { ...@@ -29,9 +29,9 @@ void main() {
expect(find.text('World'), findsOneWidget); expect(find.text('World'), findsOneWidget);
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World'); expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[widget], views: <Widget>[widget],
), ),
); );
...@@ -40,9 +40,9 @@ void main() { ...@@ -40,9 +40,9 @@ void main() {
expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World'); expect(tester.renderObject<RenderParagraph>(find.byType(Text)).text.toPlainText(), 'World');
tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'FooBar'; tester.state<TestWidgetState>(find.byType(TestWidget)).text = 'FooBar';
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: widget, widget,
); );
expect(find.text('World'), findsNothing); expect(find.text('World'), findsNothing);
expect(find.text('FooBar'), findsOneWidget); expect(find.text('FooBar'), findsOneWidget);
...@@ -65,9 +65,9 @@ void main() { ...@@ -65,9 +65,9 @@ void main() {
child: TestWidget(key: key2), child: TestWidget(key: key2),
); );
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[view1, view2], views: <Widget>[view1, view2],
), ),
); );
...@@ -91,9 +91,9 @@ void main() { ...@@ -91,9 +91,9 @@ void main() {
expect(renderParagraphTexts(), <String>['Guten', 'Abend']); expect(renderParagraphTexts(), <String>['Guten', 'Abend']);
tester.state<TestWidgetState>(find.byKey(key2)).text = 'Morgen'; tester.state<TestWidgetState>(find.byKey(key2)).text = 'Morgen';
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[view1, ViewCollection(views: <Widget>[view2])], views: <Widget>[view1, ViewCollection(views: <Widget>[view2])],
), ),
); );
...@@ -202,9 +202,3 @@ class TestWidgetState extends State<TestWidget> { ...@@ -202,9 +202,3 @@ class TestWidgetState extends State<TestWidget> {
return Text(text, textDirection: TextDirection.ltr); return Text(text, textDirection: TextDirection.ltr);
} }
} }
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
...@@ -77,9 +77,9 @@ void main() { ...@@ -77,9 +77,9 @@ void main() {
PipelineOwner? outsideParent; PipelineOwner? outsideParent;
PipelineOwner? insideParent; PipelineOwner? insideParent;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
outsideView = View.maybeOf(context); outsideView = View.maybeOf(context);
outsideParent = View.pipelineOwnerOf(context); outsideParent = View.pipelineOwnerOf(context);
...@@ -114,9 +114,9 @@ void main() { ...@@ -114,9 +114,9 @@ void main() {
}); });
testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async { testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[ views: <Widget>[
View( View(
view: tester.view, view: tester.view,
...@@ -224,9 +224,9 @@ void main() { ...@@ -224,9 +224,9 @@ void main() {
}); });
testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async { testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[ views: <Widget>[
View( View(
view: tester.view, view: tester.view,
...@@ -250,9 +250,9 @@ void main() { ...@@ -250,9 +250,9 @@ void main() {
}); });
expect(children, hasLength(3)); expect(children, hasLength(3));
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: ViewCollection( ViewCollection(
views: <Widget>[ views: <Widget>[
View( View(
view: tester.view, view: tester.view,
...@@ -271,9 +271,9 @@ void main() { ...@@ -271,9 +271,9 @@ void main() {
group('renderObject getter', () { group('renderObject getter', () {
testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async { testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async {
late BuildContext builderContext; late BuildContext builderContext;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
builderContext = context; builderContext = context;
return View( return View(
...@@ -293,9 +293,9 @@ void main() { ...@@ -293,9 +293,9 @@ void main() {
testWidgets('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async { testWidgets('ancestors of ViewCollection get null for renderObject', (WidgetTester tester) async {
late BuildContext builderContext; late BuildContext builderContext;
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: Builder( Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
builderContext = context; builderContext = context;
return ViewCollection( return ViewCollection(
...@@ -345,9 +345,9 @@ void main() { ...@@ -345,9 +345,9 @@ void main() {
}); });
testWidgets('correctly switches between view configurations', (WidgetTester tester) async { testWidgets('correctly switches between view configurations', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: View( View(
view: tester.view, view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
...@@ -359,9 +359,9 @@ void main() { ...@@ -359,9 +359,9 @@ void main() {
expect(renderView.owner, same(tester.binding.pipelineOwner)); expect(renderView.owner, same(tester.binding.pipelineOwner));
expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner)); expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner));
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: View( View(
view: tester.view, view: tester.view,
child: const SizedBox(), child: const SizedBox(),
), ),
...@@ -371,9 +371,9 @@ void main() { ...@@ -371,9 +371,9 @@ void main() {
expect(renderView.owner, isNot(same(tester.binding.pipelineOwner))); expect(renderView.owner, isNot(same(tester.binding.pipelineOwner)));
expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner))); expect(tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner)));
await pumpWidgetWithoutViewWrapper( await tester.pumpWidget(
tester: tester, wrapWithView: false,
widget: View( View(
view: tester.view, view: tester.view,
deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner,
deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView,
...@@ -513,12 +513,6 @@ void main() { ...@@ -513,12 +513,6 @@ void main() {
}); });
} }
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class SpyRenderWidget extends SizedBox { class SpyRenderWidget extends SizedBox {
const SpyRenderWidget({super.key, required this.label, required this.log, super.child}); const SpyRenderWidget({super.key, required this.label, required this.log, super.child});
......
...@@ -581,15 +581,23 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -581,15 +581,23 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// By default, the provided `widget` is rendered into [WidgetTester.view],
/// whose properties tests can modify to simulate different scenarios (e.g.
/// running on a large/small screen). Tests that want to control the
/// [FlutterView] into which content is rendered can set `wrapWithView` to
/// false and use [View] widgets in the provided `widget` tree to specify the
/// desired [FlutterView]s.
///
/// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how /// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
/// this method works when the test is run with `flutter run`. /// this method works when the test is run with `flutter run`.
Future<void> pumpWidget( Future<void> pumpWidget(
Widget widget, { Widget widget, {
Duration? duration, Duration? duration,
EnginePhase phase = EnginePhase.sendSemanticsUpdate, EnginePhase phase = EnginePhase.sendSemanticsUpdate,
bool wrapWithView = true,
}) { }) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
binding.attachRootWidget(binding.wrapWithDefaultView(widget)); binding.attachRootWidget(wrapWithView ? binding.wrapWithDefaultView(widget) : widget);
binding.scheduleFrame(); binding.scheduleFrame();
return binding.pump(duration, phase); return binding.pump(duration, phase);
}); });
......
...@@ -112,7 +112,8 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie ...@@ -112,7 +112,8 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie
), ),
]; ];
tester.binding.attachRootWidget( return tester.pumpWidget(
wrapWithView: false,
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ViewCollection( child: ViewCollection(
...@@ -120,6 +121,4 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie ...@@ -120,6 +121,4 @@ Future<void> pumpViews({required WidgetTester tester, required List<Widget> vie
), ),
), ),
); );
tester.binding.scheduleFrame();
return tester.binding.pump();
} }
...@@ -208,7 +208,8 @@ Future<void> pumpViews({required WidgetTester tester}) { ...@@ -208,7 +208,8 @@ Future<void> pumpViews({required WidgetTester tester}) {
), ),
]; ];
tester.binding.attachRootWidget( return tester.pumpWidget(
wrapWithView: false,
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: ViewCollection( child: ViewCollection(
...@@ -216,6 +217,4 @@ Future<void> pumpViews({required WidgetTester tester}) { ...@@ -216,6 +217,4 @@ Future<void> pumpViews({required WidgetTester tester}) {
), ),
), ),
); );
tester.binding.scheduleFrame();
return tester.binding.pump();
} }
...@@ -16,6 +16,8 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -16,6 +16,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:matcher/expect.dart' as matcher; import 'package:matcher/expect.dart' as matcher;
import 'package:matcher/src/expect/async_matcher.dart'; import 'package:matcher/src/expect/async_matcher.dart';
import 'multi_view_testing.dart';
void main() { void main() {
group('expectLater', () { group('expectLater', () {
testWidgets('completes when matcher completes', (WidgetTester tester) async { testWidgets('completes when matcher completes', (WidgetTester tester) async {
...@@ -638,6 +640,56 @@ void main() { ...@@ -638,6 +640,56 @@ void main() {
.checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue); .checkMockMessageHandler(SystemChannels.accessibility.name, null), isTrue);
}); });
}); });
testWidgets('wrapWithView: false does not include View', (WidgetTester tester) async {
FlutterView? flutterView;
View? view;
int builderCount = 0;
await tester.pumpWidget(
wrapWithView: false,
Builder(
builder: (BuildContext context) {
builderCount++;
flutterView = View.maybeOf(context);
view = context.findAncestorWidgetOfExactType<View>();
return const ViewCollection(views: <Widget>[]);
},
),
);
expect(builderCount, 1);
expect(view, isNull);
expect(flutterView, isNull);
expect(find.byType(View), findsNothing);
});
testWidgets('passing a view to pumpWidget with wrapWithView: true throws', (WidgetTester tester) async {
await tester.pumpWidget(
View(
view: FakeView(tester.view),
child: const SizedBox.shrink(),
),
);
expect(
tester.takeException(),
isFlutterError.having(
(FlutterError e) => e.message,
'message',
contains('consider setting the "wrapWithView" parameter of that method to false'),
),
);
});
testWidgets('can pass a View to pumpWidget when wrapWithView: false', (WidgetTester tester) async {
await tester.pumpWidget(
wrapWithView: false,
View(
view: tester.view,
child: const SizedBox.shrink(),
),
);
expect(find.byType(View), findsOne);
});
} }
class FakeMatcher extends AsyncMatcher { class FakeMatcher extends AsyncMatcher {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment