// 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. // @dart = 2.8 import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.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), ), Container( child: const 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: Container( 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 ← Container ← 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, this.string, 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, Widget child, }) : super(key: key, child: child); @override RenderOne createRenderObject(BuildContext context) => RenderOne(); } class AnotherAncestorWidget extends SingleChildRenderObjectWidget { const AnotherAncestorWidget({ Key key, 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(); } }