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

import 'package:flutter/rendering.dart';
6
import 'package:flutter_test/flutter_test.dart';
7 8 9 10

import 'rendering_tester.dart';

class RenderLayoutTestBox extends RenderProxyBox {
11 12 13
  RenderLayoutTestBox(this.onLayout, {
    this.onPerformLayout,
  });
14

15
  final VoidCallback onLayout;
16
  final VoidCallback? onPerformLayout;
17 18

  @override
19
  void layout(Constraints constraints, { bool parentUsesSize = false }) {
20 21 22 23 24 25 26 27
    // Doing this in tests is ok, but if you're writing your own
    // render object, you want to override performLayout(), not
    // layout(). Overriding layout() would remove many critical
    // performance optimizations of the rendering system, as well as
    // many bypassing many checked-mode integrity checks.
    super.layout(constraints, parentUsesSize: parentUsesSize);
    onLayout();
  }
28 29

  @override
30
  bool get sizedByParent => true;
31 32

  @override
33 34 35 36
  void performLayout() {
    child?.layout(constraints, parentUsesSize: true);
    onPerformLayout?.call();
  }
37 38 39
}

void main() {
40 41
  TestRenderingFlutterBinding.ensureInitialized();

42 43 44 45
  test('moving children', () {
    RenderBox child1, child2;
    bool movedChild1 = false;
    bool movedChild2 = false;
46 47 48
    final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
    block.add(child1 = RenderLayoutTestBox(() { movedChild1 = true; }));
    block.add(child2 = RenderLayoutTestBox(() { movedChild2 = true; }));
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    layout(block);
    expect(movedChild1, isTrue);
    expect(movedChild2, isTrue);

    movedChild1 = false;
    movedChild2 = false;

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);

    block.move(child1, after: child2);
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isTrue);
    expect(movedChild2, isTrue);

    movedChild1 = false;
    movedChild2 = false;

    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
    pumpFrame();
    expect(movedChild1, isFalse);
    expect(movedChild2, isFalse);
  });
81 82 83 84 85 86 87 88 89 90 91 92 93 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206

  group('Throws when illegal mutations are attempted: ', () {
    FlutterError catchLayoutError(RenderBox box) {
      Object? error;
      layout(box, onErrors: () {
        error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception;
      });
      expect(error, isFlutterError);
      return error! as FlutterError;
    }

    test('on disposed render objects', () {
      final RenderBox box = RenderLayoutTestBox(() {});
      box.dispose();

      Object? error;
      try {
        box.markNeedsLayout();
      } catch (e) {
        error = e;
      }

      expect(error, isFlutterError);
      expect(
        (error! as FlutterError).message,
        equalsIgnoringWhitespace(
          'A disposed RenderObject was mutated.\n'
          'The disposed RenderObject was:\n'
          '${box.toStringShort()}'
        )
      );
    });

    test('marking itself dirty in performLayout', () {
      late RenderBox child1;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in its own performLayout implementation.\n'
          'A RenderObject must not re-dirty itself while still being laid out.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'Consider using the LayoutBuilder widget to dynamically change a subtree during layout.'
        )
      );
    });

    test('marking a sibling dirty in performLayout', () {
      late RenderBox child1, child2;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(child2 = RenderLayoutTestBox(() {}, onPerformLayout: () { child1.markNeedsLayout(); }));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
          'A RenderObject must not mutate another RenderObject from a different render subtree in its performLayout method.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${child2.toStringShort()}\n'
          'Their common ancestor was:\n'
          '${block.toStringShort()}\n'
          'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
        )
      );
    });

    test('marking a descendant dirty in performLayout', () {
      late RenderBox child1;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(RenderLayoutTestBox(child1.markNeedsLayout));

      expect(
        catchLayoutError(block).message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderFlex.performLayout.\n'
          'A RenderObject must not mutate its descendants in its performLayout method.\n'
          'The RenderObject being mutated was:\n'
          '${child1.toStringShort()}\n'
          'The ancestor RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${block.toStringShort()}\n'
          'Mutating the layout of another RenderObject may cause some RenderObjects in its subtree to be laid out more than once. Consider using the LayoutBuilder widget to dynamically mutate a subtree during layout.'
        ),
      );
    });

    test('marking an out-of-band mutation in performLayout', () {
      late RenderProxyBox child1, child11, child2, child21;
      final RenderFlex block = RenderFlex(textDirection: TextDirection.ltr);
      block.add(child1 = RenderLayoutTestBox(() {}));
      block.add(child2 = RenderLayoutTestBox(() {}));
      child1.child = child11 = RenderLayoutTestBox(() {});
      layout(block);

      expect(block.debugNeedsLayout, false);
      expect(child1.debugNeedsLayout, false);
      expect(child11.debugNeedsLayout, false);
      expect(child2.debugNeedsLayout, false);

      // Add a new child to child2 which is a relayout boundary.
      child2.child = child21 = RenderLayoutTestBox(() {}, onPerformLayout: child11.markNeedsLayout);

      FlutterError? error;
      pumpFrame(onErrors: () {
        error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!.exception as FlutterError;
      });

      expect(
        error?.message,
        equalsIgnoringWhitespace(
          'A RenderLayoutTestBox was mutated in RenderLayoutTestBox.performLayout.\n'
          'The RenderObject was mutated when none of its ancestors is actively performing layout.\n'
          'The RenderObject being mutated was:\n'
          '${child11.toStringShort()}\n'
          'The RenderObject that was mutating the said RenderLayoutTestBox was:\n'
          '${child21.toStringShort()}'
        ),
      );
    });
  });
207
}