custom_multi_child_layout_test.dart 14.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hixie's avatar
Hixie committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Adam Barth's avatar
Adam Barth committed
5
import 'package:flutter_test/flutter_test.dart';
6
import 'package:flutter/foundation.dart';
7 8 9 10
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate {
11
  late BoxConstraints getSizeConstraints;
12

13
  @override
14
  Size getSize(BoxConstraints constraints) {
15
    if (!RenderObject.debugCheckingIntrinsics)
16
      getSizeConstraints = constraints;
17
    return const Size(200.0, 300.0);
18 19
  }

20 21 22 23
  Size? performLayoutSize;
  late Size performLayoutSize0;
  late Size performLayoutSize1;
  late bool performLayoutIsChild;
24

25
  @override
26
  void performLayout(Size size) {
27
    assert(!RenderObject.debugCheckingIntrinsics);
28 29
    expect(() {
      performLayoutSize = size;
30
      final BoxConstraints constraints = BoxConstraints.loose(size);
31 32
      performLayoutSize0 = layoutChild(0, constraints);
      performLayoutSize1 = layoutChild(1, constraints);
33
      performLayoutIsChild = hasChild('fred');
34
    }, returnsNormally);
35
  }
36 37 38

  bool shouldRelayoutCalled = false;
  bool shouldRelayoutValue = false;
39 40

  @override
41
  bool shouldRelayout(_) {
42
    assert(!RenderObject.debugCheckingIntrinsics);
43 44 45
    shouldRelayoutCalled = true;
    return shouldRelayoutValue;
  }
46 47
}

48
Widget buildFrame(MultiChildLayoutDelegate delegate) {
49 50
  return Center(
    child: CustomMultiChildLayout(
51
      children: <Widget>[
52 53
        LayoutId(id: 0, child: const SizedBox(width: 150.0, height: 100.0)),
        LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)),
54
      ],
55 56
      delegate: delegate,
    ),
57 58 59
  );
}

60
class PreferredSizeDelegate extends MultiChildLayoutDelegate {
61
  PreferredSizeDelegate({ required this.preferredSize });
62 63 64

  final Size preferredSize;

65
  @override
66 67
  Size getSize(BoxConstraints constraints) => preferredSize;

68
  @override
69 70
  void performLayout(Size size) { }

71
  @override
72 73 74 75
  bool shouldRelayout(PreferredSizeDelegate oldDelegate) {
    return preferredSize != oldDelegate.preferredSize;
  }
}
76

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
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;
  }
}

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
// 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, const Offset(0, 0));
    positionChild(1, const Offset(0, 0));
  }

  @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;
}

151
class LayoutWithMissingId extends ParentDataWidget<MultiChildLayoutParentData> {
152
  const LayoutWithMissingId({
153 154
    Key? key,
    required Widget child,
155 156 157 158 159
  }) : assert(child != null),
       super(key: key, child: child);

  @override
  void applyParentData(RenderObject renderObject) {}
160 161 162

  @override
  Type get debugTypicalAncestorWidgetClass => CustomMultiChildLayout;
163 164
}

165
void main() {
166
  testWidgets('Control test for CustomMultiChildLayout', (WidgetTester tester) async {
167
    final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate();
168
    await tester.pumpWidget(buildFrame(delegate));
169 170 171 172 173 174

    expect(delegate.getSizeConstraints.minWidth, 0.0);
    expect(delegate.getSizeConstraints.maxWidth, 800.0);
    expect(delegate.getSizeConstraints.minHeight, 0.0);
    expect(delegate.getSizeConstraints.maxHeight, 600.0);

175 176
    expect(delegate.performLayoutSize!.width, 200.0);
    expect(delegate.performLayoutSize!.height, 300.0);
177 178 179 180 181
    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);
182
  });
183

184
  testWidgets('Test MultiChildDelegate shouldRelayout method', (WidgetTester tester) async {
185
    TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate();
186
    await tester.pumpWidget(buildFrame(delegate));
187 188 189 190 191 192

    // 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.
193
    delegate = TestMultiChildLayoutDelegate();
194
    delegate.shouldRelayoutValue = false;
195
    await tester.pumpWidget(buildFrame(delegate));
196 197 198 199
    expect(delegate.shouldRelayoutCalled, isTrue);
    expect(delegate.performLayoutSize, isNull);

    // Layout happened because shouldRelayout() returned true.
200
    delegate = TestMultiChildLayoutDelegate();
201
    delegate.shouldRelayoutValue = true;
202
    await tester.pumpWidget(buildFrame(delegate));
203 204
    expect(delegate.shouldRelayoutCalled, isTrue);
    expect(delegate.performLayoutSize, isNotNull);
205 206
  });

207
  testWidgets('Nested CustomMultiChildLayouts', (WidgetTester tester) async {
208 209 210
    final TestMultiChildLayoutDelegate delegate = TestMultiChildLayoutDelegate();
    await tester.pumpWidget(Center(
      child: CustomMultiChildLayout(
211
        children: <Widget>[
212
          LayoutId(
213
            id: 0,
214
            child: CustomMultiChildLayout(
215
              children: <Widget>[
216 217
                LayoutId(id: 0, child: const SizedBox(width: 150.0, height: 100.0)),
                LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)),
218
              ],
219 220
              delegate: delegate,
            ),
221
          ),
222
          LayoutId(id: 1, child: const SizedBox(width: 100.0, height: 200.0)),
223
        ],
224 225
        delegate: delegate,
      ),
226 227
    ));

228
  });
229

230
  testWidgets('Loose constraints', (WidgetTester tester) async {
231 232 233
    final Key key = UniqueKey();
    await tester.pumpWidget(Center(
      child: CustomMultiChildLayout(
234
        key: key,
235 236
        delegate: PreferredSizeDelegate(preferredSize: const Size(300.0, 200.0)),
      ),
237 238
    ));

239
    final RenderBox box = tester.renderObject(find.byKey(key));
240 241 242
    expect(box.size.width, equals(300.0));
    expect(box.size.height, equals(200.0));

243 244
    await tester.pumpWidget(Center(
      child: CustomMultiChildLayout(
245
        key: key,
246 247
        delegate: PreferredSizeDelegate(preferredSize: const Size(350.0, 250.0)),
      ),
248 249 250 251
    ));

    expect(box.size.width, equals(350.0));
    expect(box.size.height, equals(250.0));
252
  });
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273

  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)));
  });
274 275 276 277 278 279 280 281 282 283 284 285

  group('performLayout error control test', () {
    Widget buildSingleChildFrame(MultiChildLayoutDelegate delegate) {
      return Center(
        child: CustomMultiChildLayout(
          children: <Widget>[LayoutId(id: 0, child: const SizedBox())],
          delegate: delegate,
        ),
      );
    }

    Future<void> expectFlutterErrorMessage({
286 287 288 289
      Widget? widget,
      MultiChildLayoutDelegate? delegate,
      required WidgetTester tester,
      required String message,
290
    }) async {
291
      final FlutterExceptionHandler? oldHandler = FlutterError.onError;
292 293 294
      final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
      FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
      try {
295
        await tester.pumpWidget(widget ?? buildSingleChildFrame(delegate!));
296 297 298 299 300 301
      } finally {
        FlutterError.onError = oldHandler;
      }
      expect(errors.length, isNonZero);
      expect(errors.first, isNotNull);
      expect(errors.first.exception, isFlutterError);
302
      expect((errors.first.exception as FlutterError).toStringDeep(), equalsIgnoringHashCodes(message));
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    }

    testWidgets('layoutChild on non existent child', (WidgetTester tester) async {
      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 {
      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 {
      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 {
      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 {
      expectFlutterErrorMessage(
        widget: Center(
          child: CustomMultiChildLayout(
            children: <Widget>[LayoutWithMissingId(child: Container(width: 100))],
            delegate: PreferredSizeDelegate(preferredSize: const Size(10, 10)),
          ),
        ),
        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 {
      expectFlutterErrorMessage(
        widget: Center(
          child: CustomMultiChildLayout(
            children: <Widget>[
              LayoutId(id: 0, child: Container(width: 100)),
              LayoutId(id: 1, child: Container(width: 100)),
              LayoutId(id: 2, child: Container(width: 100)),
            ],
            delegate: ZeroAndOneIdLayoutDelegate(),
          ),
        ),
        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 {
      expectFlutterErrorMessage(
        widget: Center(
          child: CustomMultiChildLayout(
            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)),
            ],
            delegate: ZeroAndOneIdLayoutDelegate(),
          ),
        ),
        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'
      );
    });
  });
429
}