Unverified Commit a34e4194 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Inject current `FlutterView` into tree and make available via `View.of(context)` (#116924)

* enable View.of

* tests

* ++

* greg review

* rewording

* hide view from public
parent 86b62a3c
...@@ -19,6 +19,7 @@ import 'framework.dart'; ...@@ -19,6 +19,7 @@ import 'framework.dart';
import 'platform_menu_bar.dart'; import 'platform_menu_bar.dart';
import 'router.dart'; import 'router.dart';
import 'service_extensions.dart'; import 'service_extensions.dart';
import 'view.dart';
import 'widget_inspector.dart'; import 'widget_inspector.dart';
export 'dart:ui' show AppLifecycleState, Locale; export 'dart:ui' show AppLifecycleState, Locale;
...@@ -896,6 +897,22 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -896,6 +897,22 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
@override @override
bool get framesEnabled => super.framesEnabled && _readyToProduceFrames; 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. /// Schedules a [Timer] for attaching the root widget.
/// ///
/// This is called by [runApp] to configure the widget tree. Consider using /// This is called by [runApp] to configure the widget tree. Consider using
...@@ -1014,8 +1031,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB ...@@ -1014,8 +1031,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
/// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to /// * [WidgetsBinding.handleBeginFrame], which pumps the widget pipeline to
/// 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) {
WidgetsFlutterBinding.ensureInitialized() final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized();
..scheduleAttachRootWidget(app) binding
..scheduleAttachRootWidget(binding.wrapWithDefaultView(app))
..scheduleWarmUpFrame(); ..scheduleWarmUpFrame();
} }
......
// 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<View>(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<View>(context);
final List<DiagnosticsNode> information = <DiagnosticsNode>[
if (hiddenByBoundary) ...<DiagnosticsNode>[
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 ...<DiagnosticsNode>[
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!;
}
}
// 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._();
}
...@@ -148,8 +148,11 @@ export 'src/widgets/transitions.dart'; ...@@ -148,8 +148,11 @@ export 'src/widgets/transitions.dart';
export 'src/widgets/tween_animation_builder.dart'; export 'src/widgets/tween_animation_builder.dart';
export 'src/widgets/unique_widget.dart'; export 'src/widgets/unique_widget.dart';
export 'src/widgets/value_listenable_builder.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/viewport.dart';
export 'src/widgets/visibility.dart'; export 'src/widgets/visibility.dart';
export 'src/widgets/widget_inspector.dart'; export 'src/widgets/widget_inspector.dart';
export 'src/widgets/widget_span.dart'; export 'src/widgets/widget_span.dart';
export 'src/widgets/will_pop_scope.dart'; export 'src/widgets/will_pop_scope.dart';
export 'src/widgets/window.dart';
...@@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -8,7 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('debugCheckHasMaterial control test', (WidgetTester tester) async { 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(); final dynamic exception = tester.takeException();
expect(exception, isFlutterError); expect(exception, isFlutterError);
final FlutterError error = exception as FlutterError; final FlutterError error = exception as FlutterError;
...@@ -25,7 +25,7 @@ void main() { ...@@ -25,7 +25,7 @@ void main() {
expect(error.diagnostics[3], isA<DiagnosticsProperty<Element>>()); expect(error.diagnostics[3], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics[4], isA<DiagnosticsBlock>()); expect(error.diagnostics[4], isA<DiagnosticsBlock>());
expect( expect(
error.toStringDeep(), error.toStringDeep(), startsWith(
'FlutterError\n' 'FlutterError\n'
' No Material widget found.\n' ' No Material widget found.\n'
' Chip widgets require a Material widget ancestor within the\n' ' Chip widgets require a Material widget ancestor within the\n'
...@@ -42,12 +42,13 @@ void main() { ...@@ -42,12 +42,13 @@ void main() {
' The specific widget that could not find a Material ancestor was:\n' ' The specific widget that could not find a Material ancestor was:\n'
' Chip\n' ' Chip\n'
' The ancestors of this widget were:\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 { testWidgets('debugCheckHasMaterialLocalizations control test', (WidgetTester tester) async {
await tester.pumpWidget(const BackButton()); await tester.pumpWidget(const Center(child: BackButton()));
final dynamic exception = tester.takeException(); final dynamic exception = tester.takeException();
expect(exception, isFlutterError); expect(exception, isFlutterError);
final FlutterError error = exception as FlutterError; final FlutterError error = exception as FlutterError;
...@@ -64,7 +65,7 @@ void main() { ...@@ -64,7 +65,7 @@ void main() {
expect(error.diagnostics[4], isA<DiagnosticsProperty<Element>>()); expect(error.diagnostics[4], isA<DiagnosticsProperty<Element>>());
expect(error.diagnostics[5], isA<DiagnosticsBlock>()); expect(error.diagnostics[5], isA<DiagnosticsBlock>());
expect( expect(
error.toStringDeep(), error.toStringDeep(), startsWith(
'FlutterError\n' 'FlutterError\n'
' No MaterialLocalizations found.\n' ' No MaterialLocalizations found.\n'
' BackButton widgets require MaterialLocalizations to be provided\n' ' BackButton widgets require MaterialLocalizations to be provided\n'
...@@ -78,8 +79,9 @@ void main() { ...@@ -78,8 +79,9 @@ void main() {
' ancestor was:\n' ' ancestor was:\n'
' BackButton\n' ' BackButton\n'
' The ancestors of this widget were:\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 { testWidgets('debugCheckHasScaffold control test', (WidgetTester tester) async {
...@@ -233,6 +235,7 @@ void main() { ...@@ -233,6 +235,7 @@ void main() {
' HeroControllerScope\n' ' HeroControllerScope\n'
' ScrollConfiguration\n' ' ScrollConfiguration\n'
' MaterialApp\n' ' MaterialApp\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n' ' [root]\n'
' Typically, the Scaffold widget is introduced by the MaterialApp\n' ' Typically, the Scaffold widget is introduced by the MaterialApp\n'
' or WidgetsApp widget at the top of your application widget tree.\n' ' or WidgetsApp widget at the top of your application widget tree.\n'
...@@ -377,6 +380,7 @@ void main() { ...@@ -377,6 +380,7 @@ void main() {
' Scaffold-[LabeledGlobalKey<ScaffoldState>#00000]\n' ' Scaffold-[LabeledGlobalKey<ScaffoldState>#00000]\n'
' MediaQuery\n' ' MediaQuery\n'
' Directionality\n' ' Directionality\n'
' View-[GlobalObjectKey TestWindow#00000]\n'
' [root]\n' ' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n' ' Typically, the ScaffoldMessenger widget is introduced by the\n'
' MaterialApp at the top of your application widget tree.\n' ' MaterialApp at the top of your application widget tree.\n'
......
...@@ -2458,6 +2458,7 @@ void main() { ...@@ -2458,6 +2458,7 @@ void main() {
' Scaffold\n' ' Scaffold\n'
' MediaQuery\n' ' MediaQuery\n'
' Directionality\n' ' Directionality\n'
' View-[GlobalObjectKey TestWindow#e6136]\n'
' [root]\n' ' [root]\n'
' Typically, the ScaffoldMessenger widget is introduced by the\n' ' Typically, the ScaffoldMessenger widget is introduced by the\n'
' MaterialApp at the top of your application widget tree.\n', ' MaterialApp at the top of your application widget tree.\n',
......
...@@ -7390,7 +7390,6 @@ void main() { ...@@ -7390,7 +7390,6 @@ void main() {
final dynamic exception = tester.takeException(); final dynamic exception = tester.takeException();
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect(exception.toString(), startsWith('No Material widget found.')); 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 { testWidgets('TextField loses focus when disabled', (WidgetTester tester) async {
......
...@@ -372,7 +372,8 @@ void main() { ...@@ -372,7 +372,8 @@ void main() {
' in its parent data.\n' ' in its parent data.\n'
' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n' ' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n'
' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\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' ' parentData: offset=Offset(0.0, 0.0); id=null\n'
' constraints: MISSING\n' ' constraints: MISSING\n'
' size: MISSING\n' ' size: MISSING\n'
......
...@@ -79,14 +79,13 @@ void main() { ...@@ -79,14 +79,13 @@ void main() {
expect(error.diagnostics[2], isA<DiagnosticsProperty<Element>>()); expect(error.diagnostics[2], isA<DiagnosticsProperty<Element>>());
expect( expect(
error.toStringDeep(), error.toStringDeep(),
equalsIgnoringHashCodes( startsWith(
'FlutterError\n' 'FlutterError\n'
' No Table widget found.\n' ' No Table widget found.\n'
' Builder widgets require a Table widget ancestor.\n' ' Builder widgets require a Table widget ancestor.\n'
' The specific widget that could not find a Table ancestor was:\n' ' The specific widget that could not find a Table ancestor was:\n'
' Builder\n' ' Builder\n'
' The ownership chain for the affected widget is: "Builder ←\n' ' The ownership chain for the affected widget is: "Builder ←', // End of ownership chain omitted, not relevant for test.
' [root]"\n',
), ),
); );
} }
...@@ -122,15 +121,20 @@ void main() { ...@@ -122,15 +121,20 @@ void main() {
); );
expect( expect(
error.toStringDeep(), error.toStringDeep(),
equalsIgnoringHashCodes( startsWith(
'FlutterError\n' 'FlutterError\n'
' No MediaQuery widget ancestor found.\n' ' No MediaQuery widget ancestor found.\n'
' Builder widgets require a MediaQuery widget ancestor.\n' ' Builder widgets require a MediaQuery widget ancestor.\n'
' The specific widget that could not find a MediaQuery ancestor\n' ' The specific widget that could not find a MediaQuery ancestor\n'
' was:\n' ' was:\n'
' Builder\n' ' Builder\n'
' The ownership chain for the affected widget is: "Builder ←\n' ' The ownership chain for the affected widget is: "Builder ←' // Full chain omitted, not relevant for test.
' [root]"\n' ),
);
expect(
error.toStringDeep(),
endsWith(
'[root]"\n' // End of ownership chain.
' No MediaQuery ancestor could be found starting from the context\n' ' No MediaQuery ancestor could be found starting from the context\n'
' that was passed to MediaQuery.of(). This can happen because you\n' ' that was passed to MediaQuery.of(). This can happen because you\n'
' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n' ' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n'
......
...@@ -1228,7 +1228,8 @@ void main() { ...@@ -1228,7 +1228,8 @@ void main() {
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'FocusManager#00000\n' 'FocusManager#00000\n'
' │ primaryFocus: FocusNode#00000(Child 4 [PRIMARY FOCUS])\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' ' │\n'
' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n' ' └─rootScope: FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
' │ IN FOCUS PATH\n' ' │ IN FOCUS PATH\n'
...@@ -1266,7 +1267,6 @@ void main() { ...@@ -1266,7 +1267,6 @@ void main() {
}); });
}); });
group('Autofocus', () { group('Autofocus', () {
testWidgets( testWidgets(
'works when the previous focused node is detached', 'works when the previous focused node is detached',
...@@ -1695,7 +1695,6 @@ void main() { ...@@ -1695,7 +1695,6 @@ void main() {
debugPrint = oldDebugPrint; debugPrint = oldDebugPrint;
} }
final String messagesStr = messages.toString(); 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(RegExp(r' └─Child 1: FocusScopeNode#[a-f0-9]{5}\(parent1 \[PRIMARY FOCUS\]\)')));
expect(messagesStr, contains('FOCUS: Notified 2 dirty nodes')); 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'))); expect(messagesStr, contains(RegExp(r'FOCUS: Scheduling update, current focus is null, next focus will be FocusScopeNode#.*parent1')));
......
...@@ -1183,13 +1183,13 @@ void main() { ...@@ -1183,13 +1183,13 @@ void main() {
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
equalsIgnoringHashCodes( startsWith(
'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n' 'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n'
'This typically means that the `_EmptyWidget` or its children\n' 'This typically means that the `_EmptyWidget` or its children\n'
'are not a subtype of `RenderObjectWidget`.\n' 'are not a subtype of `RenderObjectWidget`.\n'
'The following element does not have an associated render object:\n' 'The following element does not have an associated render object:\n'
' _EmptyWidget\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() { ...@@ -1216,13 +1216,13 @@ void main() {
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
equalsIgnoringHashCodes( startsWith(
'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n' 'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n'
'This typically means that the `_EmptyWidget` or its children\n' 'This typically means that the `_EmptyWidget` or its children\n'
'are not a subtype of `RenderObjectWidget`.\n' 'are not a subtype of `RenderObjectWidget`.\n'
'The following element does not have an associated render object:\n' 'The following element does not have an associated render object:\n'
' _EmptyWidget\n' ' _EmptyWidget\n'
'debugCreator: _EmptyWidget ← Column ← [root]', 'debugCreator: _EmptyWidget ← Column ← ', // Omitted end of debugCreator chain because it's irrelevant for test.
), ),
); );
}); });
......
...@@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> { ...@@ -30,7 +30,7 @@ class TestWidgetState extends State<TestWidget> {
void main() { void main() {
testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async { testWidgets('initState() is called when we are in the tree', (WidgetTester tester) async {
await tester.pumpWidget(const Parent(child: TestWidget())); await tester.pumpWidget(const Parent(child: TestWidget()));
expect(ancestors, equals(<String>['Parent', 'RenderObjectToWidgetAdapter<RenderBox>'])); expect(ancestors, equals(<String>['Parent', 'View', 'RenderObjectToWidgetAdapter<RenderBox>']));
}); });
} }
......
...@@ -63,15 +63,20 @@ void main() { ...@@ -63,15 +63,20 @@ void main() {
expect(error.diagnostics.last, isA<ErrorHint>()); expect(error.diagnostics.last, isA<ErrorHint>());
expect( expect(
error.toStringDeep(), error.toStringDeep(),
equalsIgnoringHashCodes( startsWith(
'FlutterError\n' 'FlutterError\n'
' No MediaQuery widget ancestor found.\n' ' No MediaQuery widget ancestor found.\n'
' Builder widgets require a MediaQuery widget ancestor.\n' ' Builder widgets require a MediaQuery widget ancestor.\n'
' The specific widget that could not find a MediaQuery ancestor\n' ' The specific widget that could not find a MediaQuery ancestor\n'
' was:\n' ' was:\n'
' Builder\n' ' Builder\n'
' The ownership chain for the affected widget is: "Builder ←\n' ' The ownership chain for the affected widget is: "Builder ←', // Full ownership chain omitted, not relevant for test.
' [root]"\n' ),
);
expect(
error.toStringDeep(),
endsWith(
'[root]"\n' // End of ownership chain.
' No MediaQuery ancestor could be found starting from the context\n' ' No MediaQuery ancestor could be found starting from the context\n'
' that was passed to MediaQuery.of(). This can happen because you\n' ' that was passed to MediaQuery.of(). This can happen because you\n'
' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n' ' have not added a WidgetsApp, CupertinoApp, or MaterialApp widget\n'
......
...@@ -274,7 +274,7 @@ void main() { ...@@ -274,7 +274,7 @@ void main() {
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
equalsIgnoringHashCodes( startsWith(
'Incorrect use of ParentDataWidget.\n' 'Incorrect use of ParentDataWidget.\n'
'The following ParentDataWidgets are providing parent data to the same RenderObject:\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' '- Positioned(left: 7.0, top: 6.0) (typically placed directly inside a Stack widget)\n'
...@@ -283,7 +283,7 @@ void main() { ...@@ -283,7 +283,7 @@ void main() {
'Usually, this indicates that at least one of the offending ParentDataWidgets listed ' 'Usually, this indicates that at least one of the offending ParentDataWidgets listed '
'above is not placed directly inside a compatible ancestor widget.\n' 'above is not placed directly inside a compatible ancestor widget.\n'
'The ownership chain for the RenderObject that received the parent data was:\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() { ...@@ -311,7 +311,7 @@ void main() {
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
equalsIgnoringHashCodes( startsWith(
'Incorrect use of ParentDataWidget.\n' 'Incorrect use of ParentDataWidget.\n'
'The ParentDataWidget Positioned(left: 7.0, top: 6.0) wants to apply ParentData of type ' '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 ' 'StackParentData to a RenderObject, which has been set up to accept ParentData of '
...@@ -320,7 +320,7 @@ void main() { ...@@ -320,7 +320,7 @@ void main() {
'Typically, Positioned widgets are placed directly inside Stack widgets.\n' 'Typically, Positioned widgets are placed directly inside Stack widgets.\n'
'The offending Positioned is currently placed inside a Row widget.\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' '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() { ...@@ -410,7 +410,7 @@ void main() {
expect(exception, isFlutterError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
equalsIgnoringHashCodes( startsWith(
'Incorrect use of ParentDataWidget.\n' 'Incorrect use of ParentDataWidget.\n'
'The ParentDataWidget Expanded(flex: 1) wants to apply ParentData of type ' 'The ParentDataWidget Expanded(flex: 1) wants to apply ParentData of type '
'FlexParentData to a RenderObject, which has been set up to accept ParentData of ' 'FlexParentData to a RenderObject, which has been set up to accept ParentData of '
...@@ -419,7 +419,7 @@ void main() { ...@@ -419,7 +419,7 @@ void main() {
'Typically, Expanded widgets are placed directly inside Flex widgets.\n' 'Typically, Expanded widgets are placed directly inside Flex widgets.\n'
'The offending Expanded is currently placed inside a Stack widget.\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' '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.
), ),
); );
}); });
......
...@@ -2486,7 +2486,7 @@ void main() { ...@@ -2486,7 +2486,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']), containsAllInOrder(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
); );
onPlatformViewCreatedCallBack(createdPlatformViewId); onPlatformViewCreatedCallBack(createdPlatformViewId);
...@@ -2495,7 +2495,7 @@ void main() { ...@@ -2495,7 +2495,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), containsAllInOrder(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']),
); );
expect(createdPlatformViewId, currentViewId + 1); expect(createdPlatformViewId, currentViewId + 1);
...@@ -2535,7 +2535,7 @@ void main() { ...@@ -2535,7 +2535,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['Center', 'SizedBox', 'PlatformViewLink', '_PlatformViewPlaceHolder']), containsAllInOrder(<String>['Center', 'SizedBox', 'PlatformViewLink', '_PlatformViewPlaceHolder']),
); );
// 'create' should not have been called by PlatformViewLink, since its // 'create' should not have been called by PlatformViewLink, since its
...@@ -2580,7 +2580,7 @@ void main() { ...@@ -2580,7 +2580,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']), containsAllInOrder(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
); );
// Layout should have triggered a create call. Simulate the callback // Layout should have triggered a create call. Simulate the callback
...@@ -2592,7 +2592,7 @@ void main() { ...@@ -2592,7 +2592,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), containsAllInOrder(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']),
); );
expect(createdPlatformViewId, currentViewId + 1); expect(createdPlatformViewId, currentViewId + 1);
...@@ -2678,7 +2678,7 @@ void main() { ...@@ -2678,7 +2678,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']), containsAllInOrder(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
); );
onPlatformViewCreatedCallBack(createdPlatformViewId); onPlatformViewCreatedCallBack(createdPlatformViewId);
...@@ -2687,7 +2687,7 @@ void main() { ...@@ -2687,7 +2687,7 @@ void main() {
expect( expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), containsAllInOrder(<String>['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']),
); );
expect(createdPlatformViewId, currentViewId + 1); expect(createdPlatformViewId, currentViewId + 1);
......
...@@ -219,27 +219,30 @@ void main() { ...@@ -219,27 +219,30 @@ void main() {
expect( expect(
tester.renderObject(find.byType(_Diagonal)).toStringDeep(), tester.renderObject(find.byType(_Diagonal)).toStringDeep(),
equalsIgnoringHashCodes(r''' equalsIgnoringHashCodes(
_RenderDiagonal#00000 relayoutBoundary=up1 '_RenderDiagonal#00000 relayoutBoundary=up1\n'
│ creator: _Diagonal ← Align ← Directionality ← [root] ' │ creator: _Diagonal ← Align ← Directionality ←\n'
│ parentData: offset=Offset(0.0, 0.0) (can use size) ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
│ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0) ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
│ size: Size(190.0, 220.0) ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n'
' │ size: Size(190.0, 220.0)\n'
├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2 ' │\n'
│ creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
│ parentData: offset=Offset(0.0, 0.0) (can use size) ' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
│ constraints: BoxConstraints(unconstrained) ' │ View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
│ size: Size(80.0, 100.0) ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n'
│ additionalConstraints: BoxConstraints(w=80.0, h=100.0) ' │ constraints: BoxConstraints(unconstrained)\n'
' │ size: Size(80.0, 100.0)\n'
└─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2 ' │ additionalConstraints: BoxConstraints(w=80.0, h=100.0)\n'
creator: SizedBox ← _Diagonal ← Align ← Directionality ← [root] ' │\n'
parentData: offset=Offset(80.0, 100.0) (can use size) ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n'
constraints: BoxConstraints(unconstrained) ' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n'
size: Size(110.0, 120.0) ' View-[GlobalObjectKey TestWindow#00000] ← [root]\n'
additionalConstraints: BoxConstraints(w=110.0, h=120.0) ' 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',
)
); );
}); });
} }
......
...@@ -783,21 +783,27 @@ void main() { ...@@ -783,21 +783,27 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Stack(), Stack(),
); );
final String exception = tester.takeException().toString();
expect( expect(
tester.takeException().toString(), exception, startsWith(
'No Directionality widget found.\n' 'No Directionality widget found.\n'
"Stack widgets require a Directionality widget ancestor to resolve the 'alignment' argument.\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 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' 'The specific widget that could not find a Directionality ancestor was:\n'
' Stack\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 ' '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 ' '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 ' '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' 'EdgeInsetsDirectional, AlignmentDirectional, and other *Directional objects.\n'
'Instead of providing a Directionality widget, another solution would be passing a non-directional ' 'Instead of providing a Directionality widget, another solution would be passing a non-directional '
"'alignment', or an explicit 'textDirection', to the Stack.", "'alignment', or an explicit 'textDirection', to the Stack.",
); ));
}); });
testWidgets('Can update clipBehavior of IndexedStack', testWidgets('Can update clipBehavior of IndexedStack',
......
// 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<FlutterView>());
expect(viewMaybeOf, isNotNull);
expect(viewMaybeOf, isA<FlutterView>());
});
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<FlutterView>());
expect(viewMaybeOf, isNotNull);
expect(viewMaybeOf, isA<FlutterView>());
});
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<FlutterError>().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.'),
)),
);
});
}
...@@ -4277,11 +4277,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService { ...@@ -4277,11 +4277,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
final Element element = tester.element(find.byType(Directionality).first); final Element element = tester.element(find.byType(Directionality).first);
Element? root; Element? root;
element.visitAncestorElements((Element ancestor) { element.visitAncestorElements((Element ancestor) {
if (root == null) { root = ancestor;
root = ancestor;
// Stop traversing ancestors.
return false;
}
return true; return true;
}); });
expect(root, isNotNull); expect(root, isNotNull);
......
...@@ -553,12 +553,24 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -553,12 +553,24 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
EnginePhase phase = EnginePhase.sendSemanticsUpdate, EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) { ]) {
return TestAsyncUtils.guard<void>(() { return TestAsyncUtils.guard<void>(() {
binding.attachRootWidget(widget); return _pumpWidget(
binding.scheduleFrame(); binding.wrapWithDefaultView(widget),
return binding.pump(duration, phase); duration,
phase,
);
}); });
} }
Future<void> _pumpWidget(
Widget widget, [
Duration? duration,
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
]) {
binding.attachRootWidget(widget);
binding.scheduleFrame();
return binding.pump(duration, phase);
}
@override @override
Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) { Future<List<Duration>> handlePointerEventRecord(Iterable<PointerEventRecord> records) {
assert(records != null); assert(records != null);
...@@ -697,7 +709,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -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. // The interval following the last frame doesn't have to be within the fullDuration.
Duration elapsed = Duration.zero; Duration elapsed = Duration.zero;
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
binding.attachRootWidget(target); binding.attachRootWidget(binding.wrapWithDefaultView(target));
binding.scheduleFrame(); binding.scheduleFrame();
while (elapsed < maxDuration) { while (elapsed < maxDuration) {
await binding.pump(interval); await binding.pump(interval);
...@@ -720,12 +732,14 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -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 ' 'therefore no restoration data has been collected to restore from. Did you forget to wrap '
'your widget tree in a RootRestorationScope?', 'your widget tree in a RootRestorationScope?',
); );
final Widget widget = ((binding.renderViewElement! as RenderObjectToWidgetElement<RenderObject>).widget as RenderObjectToWidgetAdapter<RenderObject>).child!; return TestAsyncUtils.guard<void>(() async {
final TestRestorationData restorationData = binding.restorationManager.restorationData; final Widget widget = ((binding.renderViewElement! as RenderObjectToWidgetElement<RenderObject>).widget as RenderObjectToWidgetAdapter<RenderObject>).child!;
runApp(Container(key: UniqueKey())); final TestRestorationData restorationData = binding.restorationManager.restorationData;
await pump(); runApp(Container(key: UniqueKey()));
binding.restorationManager.restoreFrom(restorationData); await pump();
return pumpWidget(widget); binding.restorationManager.restoreFrom(restorationData);
return _pumpWidget(widget);
});
} }
/// Retrieves the current restoration data from the [RestorationManager]. /// Retrieves the current restoration data from the [RestorationManager].
......
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