| // 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/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { |
| late BoxConstraints getSizeConstraints; |
| |
| @override |
| Size getSize(BoxConstraints constraints) { |
| if (!RenderObject.debugCheckingIntrinsics) |
| getSizeConstraints = constraints; |
| return const Size(200.0, 300.0); |
| } |
| |
| Size? performLayoutSize; |
| late Size performLayoutSize0; |
| late Size performLayoutSize1; |
| late bool performLayoutIsChild; |
| |
| @override |
| void performLayout(Size size) { |
| assert(!RenderObject.debugCheckingIntrinsics); |
| expect(() { |
| performLayoutSize = size; |
| final BoxConstraints constraints = BoxConstraints.loose(size); |
| performLayoutSize0 = layoutChild(0, constraints); |
| performLayoutSize1 = layoutChild(1, constraints); |
| performLayoutIsChild = hasChild('fred'); |
| }, returnsNormally); |
| } |
| |
| bool shouldRelayoutCalled = false; |
| bool shouldRelayoutValue = false; |
| |
| @override |
| bool shouldRelayout(_) { |
| assert(!RenderObject.debugCheckingIntrinsics); |
| shouldRelayoutCalled = true; |
| return shouldRelayoutValue; |
| } |
| } |
| |
| Widget buildFrame(MultiChildLayoutDelegate delegate) { |
| return Center( |
| child: CustomMultiChildLayout( |
| delegate: delegate, |
| children: <Widget>[ |
| LayoutId(id: 0, child: const SizedBox(width: 150.0, height: 100.0)), |
| LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)), |
| ], |
| ), |
| ); |
| } |
| |
| class PreferredSizeDelegate extends MultiChildLayoutDelegate { |
| PreferredSizeDelegate({ required this.preferredSize }); |
| |
| final Size preferredSize; |
| |
| @override |
| Size getSize(BoxConstraints constraints) => preferredSize; |
| |
| @override |
| void performLayout(Size size) { } |
| |
| @override |
| bool shouldRelayout(PreferredSizeDelegate oldDelegate) { |
| return preferredSize != oldDelegate.preferredSize; |
| } |
| } |
| |
| class NotifierLayoutDelegate extends MultiChildLayoutDelegate { |
| NotifierLayoutDelegate(this.size) : super(relayout: size); |
| |
| final ValueNotifier<Size> size; |
| |
| @override |
| Size getSize(BoxConstraints constraints) => size.value; |
| |
| @override |
| void performLayout(Size size) { } |
| |
| @override |
| bool shouldRelayout(NotifierLayoutDelegate oldDelegate) { |
| return size != oldDelegate.size; |
| } |
| } |
| |
| // LayoutDelegate that lays out child with id 0 and 1 |
| // Used in the 'performLayout error control test' test case to trigger: |
| // - error when laying out a non existent child and a child that has not been laid out |
| class ZeroAndOneIdLayoutDelegate extends MultiChildLayoutDelegate { |
| @override |
| void performLayout(Size size) { |
| final BoxConstraints constraints = BoxConstraints.loose(size); |
| layoutChild(0, constraints); |
| layoutChild(1, constraints); |
| } |
| |
| @override |
| bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true; |
| } |
| |
| // Used in the 'performLayout error control test' test case |
| // to trigger an error when laying out child more than once |
| class DuplicateLayoutDelegate extends MultiChildLayoutDelegate { |
| @override |
| void performLayout(Size size) { |
| final BoxConstraints constraints = BoxConstraints.loose(size); |
| layoutChild(0, constraints); |
| layoutChild(0, constraints); |
| } |
| |
| @override |
| bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true; |
| } |
| // Used in the 'performLayout error control test' test case |
| // to trigger an error when positioning non existent child |
| class NonExistentPositionDelegate extends MultiChildLayoutDelegate { |
| @override |
| void performLayout(Size size) { |
| positionChild(0, Offset.zero); |
| positionChild(1, Offset.zero); |
| } |
| |
| @override |
| bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true; |
| } |
| |
| // Used in the 'performLayout error control test' test case for triggering |
| // to layout child more than once |
| class InvalidConstraintsChildLayoutDelegate extends MultiChildLayoutDelegate { |
| @override |
| void performLayout(Size size) { |
| final BoxConstraints constraints = BoxConstraints.loose( |
| // Invalid because width and height must be greater than or equal to 0 |
| const Size(-1, -1), |
| ); |
| layoutChild(0, constraints); |
| } |
| |
| @override |
| bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true; |
| } |
| |
| class LayoutWithMissingId extends ParentDataWidget<MultiChildLayoutParentData> { |
| const LayoutWithMissingId({ |
| Key? key, |
| required Widget child, |
| }) : assert(child != null), |
| super(key: key, child: child); |
| |
| @override |
| void applyParentData(RenderObject renderObject) {} |
| |
| @override |
| Type get debugTypicalAncestorWidgetClass => CustomMultiChildLayout; |
| } |
| |
| void main() { |
| testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async { |
| final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); |
| await tester.pumpWidget(buildFrame(delegate)); |
| |
| expect(delegate.getSizeConstraints.minWidth, 0.0); |
| expect(delegate.getSizeConstraints.maxWidth, 800.0); |
| expect(delegate.getSizeConstraints.minHeight, 0.0); |
| expect(delegate.getSizeConstraints.maxHeight, 600.0); |
| |
| expect(delegate.performLayoutSize!.width, 200.0); |
| expect(delegate.performLayoutSize!.height, 300.0); |
| expect(delegate.performLayoutSize0.width, 150.0); |
| expect(delegate.performLayoutSize0.height, 100.0); |
| expect(delegate.performLayoutSize1.width, 100.0); |
| expect(delegate.performLayoutSize1.height, 200.0); |
| expect(delegate.performLayoutIsChild, false); |
| }); |
| |
| testWidgets('Test MultiChildDelegate shouldRelayout method', (WidgetTester tester) async { |
| TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); |
| await tester.pumpWidget(buildFrame(delegate)); |
| |
| // Layout happened because the delegate was set. |
| expect(delegate.performLayoutSize, isNotNull); // i.e. layout happened |
| expect(delegate.shouldRelayoutCalled, isFalse); |
| |
| // Layout did not happen because shouldRelayout() returned false. |
| delegate = TestMultiChildLayoutDelegate(); |
| delegate.shouldRelayoutValue = false; |
| await tester.pumpWidget(buildFrame(delegate)); |
| expect(delegate.shouldRelayoutCalled, isTrue); |
| expect(delegate.performLayoutSize, isNull); |
| |
| // Layout happened because shouldRelayout() returned true. |
| delegate = TestMultiChildLayoutDelegate(); |
| delegate.shouldRelayoutValue = true; |
| await tester.pumpWidget(buildFrame(delegate)); |
| expect(delegate.shouldRelayoutCalled, isTrue); |
| expect(delegate.performLayoutSize, isNotNull); |
| }); |
| |
| testWidgets('Nested CustomMultiChildLayouts', (WidgetTester tester) async { |
| final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate(); |
| await tester.pumpWidget(Center( |
| child: CustomMultiChildLayout( |
| delegate: delegate, |
| children: <Widget>[ |
| LayoutId( |
| id: 0, |
| child: CustomMultiChildLayout( |
| delegate: delegate, |
| children: <Widget>[ |
| LayoutId(id: 0, child: const SizedBox(width: 150.0, height: 100.0)), |
| LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)), |
| ], |
| ), |
| ), |
| LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)), |
| ], |
| ), |
| )); |
| |
| }); |
| |
| testWidgets('Loose constraints', (WidgetTester tester) async { |
| final Key key = UniqueKey(); |
| await tester.pumpWidget(Center( |
| child: CustomMultiChildLayout( |
| key: key, |
| delegate: PreferredSizeDelegate(preferredSize: const Size(300.0, 200.0)), |
| ), |
| )); |
| |
| final RenderBox box = tester.renderObject(find.byKey(key)); |
| expect(box.size.width, equals(300.0)); |
| expect(box.size.height, equals(200.0)); |
| |
| await tester.pumpWidget(Center( |
| child: CustomMultiChildLayout( |
| key: key, |
| delegate: PreferredSizeDelegate(preferredSize: const Size(350.0, 250.0)), |
| ), |
| )); |
| |
| expect(box.size.width, equals(350.0)); |
| expect(box.size.height, equals(250.0)); |
| }); |
| |
| testWidgets('Can use listener for relayout', (WidgetTester tester) async { |
| final ValueNotifier<Size> size = ValueNotifier<Size>(const Size(100.0, 200.0)); |
| |
| await tester.pumpWidget( |
| Center( |
| child: CustomMultiChildLayout( |
| delegate: NotifierLayoutDelegate(size), |
| ), |
| ), |
| ); |
| |
| RenderBox box = tester.renderObject(find.byType(CustomMultiChildLayout)); |
| expect(box.size, equals(const Size(100.0, 200.0))); |
| |
| size.value = const Size(150.0, 240.0); |
| await tester.pump(); |
| |
| box = tester.renderObject(find.byType(CustomMultiChildLayout)); |
| expect(box.size, equals(const Size(150.0, 240.0))); |
| }); |
| |
| group('performLayout error control test', () { |
| Widget buildSingleChildFrame(MultiChildLayoutDelegate delegate) { |
| return Center( |
| child: CustomMultiChildLayout( |
| delegate: delegate, |
| children: <Widget>[LayoutId(id: 0, child: const SizedBox())], |
| ), |
| ); |
| } |
| |
| Future<void> expectFlutterErrorMessage({ |
| Widget? widget, |
| MultiChildLayoutDelegate? delegate, |
| required WidgetTester tester, |
| required String message, |
| }) async { |
| final FlutterExceptionHandler? oldHandler = FlutterError.onError; |
| final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; |
| FlutterError.onError = (FlutterErrorDetails error) => errors.add(error); |
| try { |
| await tester.pumpWidget(widget ?? buildSingleChildFrame(delegate!)); |
| } finally { |
| FlutterError.onError = oldHandler; |
| } |
| expect(errors.length, isNonZero); |
| expect(errors.first, isNotNull); |
| expect(errors.first.exception, isFlutterError); |
| expect((errors.first.exception as FlutterError).toStringDeep(), equalsIgnoringHashCodes(message)); |
| } |
| |
| testWidgets('layoutChild on non existent child', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| tester: tester, |
| delegate: ZeroAndOneIdLayoutDelegate(), |
| message: |
| 'FlutterError\n' |
| ' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate\n' |
| ' tried to lay out a non-existent child.\n' |
| ' There is no child with the id "1".\n', |
| ); |
| }); |
| |
| testWidgets('layoutChild more than once', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| tester: tester, |
| delegate: DuplicateLayoutDelegate(), |
| message: |
| 'FlutterError\n' |
| ' The DuplicateLayoutDelegate custom multichild layout delegate\n' |
| ' tried to lay out the child with id "0" more than once.\n' |
| ' Each child must be laid out exactly once.\n', |
| ); |
| }); |
| |
| testWidgets('layoutChild on invalid size constraint', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| tester: tester, |
| delegate: InvalidConstraintsChildLayoutDelegate(), |
| message: |
| 'FlutterError\n' |
| ' The InvalidConstraintsChildLayoutDelegate custom multichild\n' |
| ' layout delegate provided invalid box constraints for the child\n' |
| ' with id "0".\n' |
| ' FlutterError\n' |
| ' The minimum width and height must be greater than or equal to\n' |
| ' zero.\n' |
| ' The maximum width must be greater than or equal to the minimum\n' |
| ' width.\n' |
| ' The maximum height must be greater than or equal to the minimum\n' |
| ' height.\n', |
| ); |
| }); |
| |
| testWidgets('positionChild on non existent child', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| tester: tester, |
| delegate: NonExistentPositionDelegate(), |
| message: |
| 'FlutterError\n' |
| ' The NonExistentPositionDelegate custom multichild layout delegate\n' |
| ' tried to position out a non-existent child:\n' |
| ' There is no child with the id "1".\n', |
| ); |
| }); |
| |
| testWidgets("_callPerformLayout on child that doesn't have id", (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| widget: Center( |
| child: CustomMultiChildLayout( |
| delegate: PreferredSizeDelegate(preferredSize: const Size(10, 10)), |
| children: <Widget>[LayoutWithMissingId(child: Container(width: 100))], |
| ), |
| ), |
| tester: tester, |
| message: |
| 'FlutterError\n' |
| ' Every child of a RenderCustomMultiChildLayoutBox must have an ID\n' |
| ' in its parent data.\n' |
| ' The following child has no ID: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT:\n' |
| ' creator: ConstrainedBox ← Container ← LayoutWithMissingId ←\n' |
| ' CustomMultiChildLayout ← Center ← [root]\n' |
| ' parentData: offset=Offset(0.0, 0.0); id=null\n' |
| ' constraints: MISSING\n' |
| ' size: MISSING\n' |
| ' additionalConstraints: BoxConstraints(w=100.0, 0.0<=h<=Infinity)\n', |
| ); |
| }); |
| |
| testWidgets('performLayout did not layout a child', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| widget: Center( |
| child: CustomMultiChildLayout( |
| delegate: ZeroAndOneIdLayoutDelegate(), |
| children: <Widget>[ |
| LayoutId(id: 0, child: Container(width: 100)), |
| LayoutId(id: 1, child: Container(width: 100)), |
| LayoutId(id: 2, child: Container(width: 100)), |
| ], |
| ), |
| ), |
| tester: tester, |
| message: |
| 'FlutterError\n' |
| ' Each child must be laid out exactly once.\n' |
| ' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate' |
| ' forgot to lay out the following child:\n' |
| ' 2: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n', |
| ); |
| }); |
| |
| testWidgets('performLayout did not layout multiple child', (WidgetTester tester) async { |
| await expectFlutterErrorMessage( |
| widget: Center( |
| child: CustomMultiChildLayout( |
| delegate: ZeroAndOneIdLayoutDelegate(), |
| children: <Widget>[ |
| LayoutId(id: 0, child: Container(width: 100)), |
| LayoutId(id: 1, child: Container(width: 100)), |
| LayoutId(id: 2, child: Container(width: 100)), |
| LayoutId(id: 3, child: Container(width: 100)), |
| ], |
| ), |
| ), |
| tester: tester, |
| message: |
| 'FlutterError\n' |
| ' Each child must be laid out exactly once.\n' |
| ' The ZeroAndOneIdLayoutDelegate custom multichild layout delegate' |
| ' forgot to lay out the following children:\n' |
| ' 2: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n' |
| ' 3: RenderConstrainedBox#62a34 NEEDS-LAYOUT NEEDS-PAINT\n', |
| ); |
| }); |
| }); |
| } |