| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'test_widgets.dart'; |
| |
| class TestParentData { |
| TestParentData({ this.top, this.right, this.bottom, this.left }); |
| |
| final double? top; |
| final double? right; |
| final double? bottom; |
| final double? left; |
| } |
| |
| void checkTree(WidgetTester tester, List<TestParentData> expectedParentData) { |
| final MultiChildRenderObjectElement element = tester.element( |
| find.byElementPredicate((Element element) => element is MultiChildRenderObjectElement), |
| ); |
| expect(element, isNotNull); |
| expect(element.renderObject, isA<RenderStack>()); |
| final RenderStack renderObject = element.renderObject as RenderStack; |
| try { |
| RenderObject? child = renderObject.firstChild; |
| for (final TestParentData expected in expectedParentData) { |
| expect(child, isA<RenderDecoratedBox>()); |
| final RenderDecoratedBox decoratedBox = child! as RenderDecoratedBox; |
| expect(decoratedBox.parentData, isA<StackParentData>()); |
| final StackParentData parentData = decoratedBox.parentData! as StackParentData; |
| expect(parentData.top, equals(expected.top)); |
| expect(parentData.right, equals(expected.right)); |
| expect(parentData.bottom, equals(expected.bottom)); |
| expect(parentData.left, equals(expected.left)); |
| final StackParentData? decoratedBoxParentData = decoratedBox.parentData as StackParentData?; |
| child = decoratedBoxParentData?.nextSibling; |
| } |
| expect(child, isNull); |
| } catch (e) { |
| print(renderObject.toStringDeep()); |
| rethrow; |
| } |
| } |
| |
| final TestParentData kNonPositioned = TestParentData(); |
| |
| void main() { |
| testWidgets('ParentDataWidget control test', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| DecoratedBox(decoration: kBoxDecorationA), |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: DecoratedBox(decoration: kBoxDecorationB), |
| ), |
| DecoratedBox(decoration: kBoxDecorationC), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| kNonPositioned, |
| TestParentData(top: 10.0, left: 10.0), |
| kNonPositioned, |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| bottom: 5.0, |
| right: 7.0, |
| child: DecoratedBox(decoration: kBoxDecorationA), |
| ), |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: DecoratedBox(decoration: kBoxDecorationB), |
| ), |
| DecoratedBox(decoration: kBoxDecorationC), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(bottom: 5.0, right: 7.0), |
| TestParentData(top: 10.0, left: 10.0), |
| kNonPositioned, |
| ]); |
| |
| const DecoratedBox kDecoratedBoxA = DecoratedBox(decoration: kBoxDecorationA); |
| const DecoratedBox kDecoratedBoxB = DecoratedBox(decoration: kBoxDecorationB); |
| const DecoratedBox kDecoratedBoxC = DecoratedBox(decoration: kBoxDecorationC); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| bottom: 5.0, |
| right: 7.0, |
| child: kDecoratedBoxA, |
| ), |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: kDecoratedBoxB, |
| ), |
| kDecoratedBoxC, |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(bottom: 5.0, right: 7.0), |
| TestParentData(top: 10.0, left: 10.0), |
| kNonPositioned, |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| bottom: 6.0, |
| right: 8.0, |
| child: kDecoratedBoxA, |
| ), |
| Positioned( |
| left: 10.0, |
| right: 10.0, |
| child: kDecoratedBoxB, |
| ), |
| kDecoratedBoxC, |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(bottom: 6.0, right: 8.0), |
| TestParentData(left: 10.0, right: 10.0), |
| kNonPositioned, |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| kDecoratedBoxA, |
| Positioned( |
| left: 11.0, |
| right: 12.0, |
| child: Container(child: kDecoratedBoxB), |
| ), |
| kDecoratedBoxC, |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| kNonPositioned, |
| TestParentData(left: 11.0, right: 12.0), |
| kNonPositioned, |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| kDecoratedBoxA, |
| Positioned( |
| right: 10.0, |
| child: Container(child: kDecoratedBoxB), |
| ), |
| const DummyWidget( |
| child: Positioned( |
| top: 8.0, |
| child: kDecoratedBoxC, |
| ), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| kNonPositioned, |
| TestParentData(right: 10.0), |
| TestParentData(top: 8.0), |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| right: 10.0, |
| child: FlipWidget(left: kDecoratedBoxA, right: kDecoratedBoxB), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(right: 10.0), |
| ]); |
| |
| flipStatefulWidget(tester); |
| await tester.pump(); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(right: 10.0), |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| top: 7.0, |
| child: FlipWidget(left: kDecoratedBoxA, right: kDecoratedBoxB), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(top: 7.0), |
| ]); |
| |
| flipStatefulWidget(tester); |
| await tester.pump(); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(top: 7.0), |
| ]); |
| |
| await tester.pumpWidget( |
| Stack(textDirection: TextDirection.ltr), |
| ); |
| |
| checkTree(tester, <TestParentData>[]); |
| }); |
| |
| testWidgets('ParentDataWidget conflicting data', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Stack( |
| textDirection: TextDirection.ltr, |
| children: const <Widget>[ |
| Positioned( |
| top: 5.0, |
| bottom: 8.0, |
| child: Positioned( |
| top: 6.0, |
| left: 7.0, |
| child: DecoratedBox(decoration: kBoxDecorationB), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| expect( |
| exception.toString(), |
| equalsIgnoringHashCodes( |
| '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' |
| '- Positioned(top: 5.0, bottom: 8.0) (typically placed directly inside a Stack widget)\n' |
| 'However, a RenderObject can only receive parent data from at most one ParentDataWidget.\n' |
| '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]', |
| ), |
| ); |
| |
| await tester.pumpWidget(Stack(textDirection: TextDirection.ltr)); |
| |
| checkTree(tester, <TestParentData>[]); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: DummyWidget( |
| child: Row( |
| children: const <Widget>[ |
| Positioned( |
| top: 6.0, |
| left: 7.0, |
| child: DecoratedBox(decoration: kBoxDecorationB), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| expect( |
| exception.toString(), |
| equalsIgnoringHashCodes( |
| '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 ' |
| 'incompatible type FlexParentData.\n' |
| 'Usually, this means that the Positioned widget has the wrong ancestor RenderObjectWidget. ' |
| '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]', |
| ), |
| ); |
| |
| await tester.pumpWidget( |
| Stack(textDirection: TextDirection.ltr), |
| ); |
| |
| checkTree(tester, <TestParentData>[]); |
| }); |
| |
| testWidgets('ParentDataWidget interacts with global keys', (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: DecoratedBox(key: key, decoration: kBoxDecorationA), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(top: 10.0, left: 10.0), |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: DecoratedBox( |
| decoration: kBoxDecorationB, |
| child: DecoratedBox(key: key, decoration: kBoxDecorationA), |
| ), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(top: 10.0, left: 10.0), |
| ]); |
| |
| await tester.pumpWidget( |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| Positioned( |
| top: 10.0, |
| left: 10.0, |
| child: DecoratedBox(key: key, decoration: kBoxDecorationA), |
| ), |
| ], |
| ), |
| ); |
| |
| checkTree(tester, <TestParentData>[ |
| TestParentData(top: 10.0, left: 10.0), |
| ]); |
| }); |
| |
| testWidgets('Parent data invalid ancestor', (WidgetTester tester) async { |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: Row( |
| children: <Widget>[ |
| Stack( |
| textDirection: TextDirection.ltr, |
| children: <Widget>[ |
| Expanded( |
| child: Container(), |
| ), |
| ], |
| ), |
| ], |
| ), |
| )); |
| |
| final dynamic exception = tester.takeException(); |
| expect(exception, isFlutterError); |
| expect( |
| exception.toString(), |
| equalsIgnoringHashCodes( |
| '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 ' |
| 'incompatible type StackParentData.\n' |
| 'Usually, this means that the Expanded widget has the wrong ancestor RenderObjectWidget. ' |
| '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]', |
| ), |
| ); |
| }); |
| |
| testWidgets('ParentDataWidget can be used with different ancestor RenderObjectWidgets', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| OneAncestorWidget( |
| child: Container(), |
| ), |
| ); |
| DummyParentData parentData = tester.renderObject(find.byType(Container)).parentData! as DummyParentData; |
| expect(parentData.string, isNull); |
| |
| await tester.pumpWidget( |
| OneAncestorWidget( |
| child: TestParentDataWidget( |
| string: 'Foo', |
| child: Container(), |
| ), |
| ), |
| ); |
| parentData = tester.renderObject(find.byType(Container)).parentData! as DummyParentData; |
| expect(parentData.string, 'Foo'); |
| |
| await tester.pumpWidget( |
| AnotherAncestorWidget( |
| child: TestParentDataWidget( |
| string: 'Bar', |
| child: Container(), |
| ), |
| ), |
| ); |
| parentData = tester.renderObject(find.byType(Container)).parentData! as DummyParentData; |
| expect(parentData.string, 'Bar'); |
| }); |
| } |
| |
| class TestParentDataWidget extends ParentDataWidget<DummyParentData> { |
| const TestParentDataWidget({ |
| Key? key, |
| required this.string, |
| required Widget child, |
| }) : super(key: key, child: child); |
| |
| final String string; |
| |
| @override |
| void applyParentData(RenderObject renderObject) { |
| assert(renderObject.parentData is DummyParentData); |
| final DummyParentData parentData = renderObject.parentData! as DummyParentData; |
| parentData.string = string; |
| } |
| |
| @override |
| Type get debugTypicalAncestorWidgetClass => OneAncestorWidget; |
| } |
| |
| class DummyParentData extends ParentData { |
| String? string; |
| } |
| |
| class OneAncestorWidget extends SingleChildRenderObjectWidget { |
| const OneAncestorWidget({ |
| Key? key, |
| required Widget child, |
| }) : super(key: key, child: child); |
| |
| @override |
| RenderOne createRenderObject(BuildContext context) => RenderOne(); |
| } |
| |
| class AnotherAncestorWidget extends SingleChildRenderObjectWidget { |
| const AnotherAncestorWidget({ |
| Key? key, |
| required Widget child, |
| }) : super(key: key, child: child); |
| |
| @override |
| RenderAnother createRenderObject(BuildContext context) => RenderAnother(); |
| } |
| |
| class RenderOne extends RenderProxyBox { |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! DummyParentData) |
| child.parentData = DummyParentData(); |
| } |
| } |
| |
| class RenderAnother extends RenderProxyBox { |
| @override |
| void setupParentData(RenderBox child) { |
| if (child.parentData is! DummyParentData) |
| child.parentData = DummyParentData(); |
| } |
| } |
| |
| class DummyWidget extends StatelessWidget { |
| const DummyWidget({ Key? key, required this.child }) : super(key: key); |
| |
| final Widget child; |
| |
| @override |
| Widget build(BuildContext context) => child; |
| } |