// 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', ); }); }); }