parent_data_test.dart 14.6 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.

5 6
// @dart = 2.8

Adam Barth's avatar
Adam Barth committed
7
import 'package:flutter_test/flutter_test.dart';
8 9
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
Adam Barth's avatar
Adam Barth committed
10 11 12 13 14 15 16 17 18 19 20 21 22

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) {
23
  final MultiChildRenderObjectElement element = tester.element(
24 25
    find.byElementPredicate((Element element) => element is MultiChildRenderObjectElement)
  );
Adam Barth's avatar
Adam Barth committed
26
  expect(element, isNotNull);
Dan Field's avatar
Dan Field committed
27
  expect(element.renderObject, isA<RenderStack>());
28
  final RenderStack renderObject = element.renderObject as RenderStack;
Adam Barth's avatar
Adam Barth committed
29 30
  try {
    RenderObject child = renderObject.firstChild;
31
    for (final TestParentData expected in expectedParentData) {
Dan Field's avatar
Dan Field committed
32
      expect(child, isA<RenderDecoratedBox>());
33
      final RenderDecoratedBox decoratedBox = child as RenderDecoratedBox;
Dan Field's avatar
Dan Field committed
34
      expect(decoratedBox.parentData, isA<StackParentData>());
35
      final StackParentData parentData = decoratedBox.parentData as StackParentData;
Adam Barth's avatar
Adam Barth committed
36 37 38 39
      expect(parentData.top, equals(expected.top));
      expect(parentData.right, equals(expected.right));
      expect(parentData.bottom, equals(expected.bottom));
      expect(parentData.left, equals(expected.left));
40
      final StackParentData decoratedBoxParentData = decoratedBox.parentData as StackParentData;
Hixie's avatar
Hixie committed
41
      child = decoratedBoxParentData.nextSibling;
Adam Barth's avatar
Adam Barth committed
42 43 44 45 46 47 48 49
    }
    expect(child, isNull);
  } catch (e) {
    print(renderObject.toStringDeep());
    rethrow;
  }
}

50
final TestParentData kNonPositioned = TestParentData();
Adam Barth's avatar
Adam Barth committed
51 52

void main() {
53 54
  testWidgets('ParentDataWidget control test', (WidgetTester tester) async {
    await tester.pumpWidget(
55
      Stack(
56
        textDirection: TextDirection.ltr,
57
        children: const <Widget>[
58 59
          DecoratedBox(decoration: kBoxDecorationA),
          Positioned(
60 61
            top: 10.0,
            left: 10.0,
62
            child: DecoratedBox(decoration: kBoxDecorationB),
63
          ),
64
          DecoratedBox(decoration: kBoxDecorationC),
65 66
        ],
      ),
67 68 69 70
    );

    checkTree(tester, <TestParentData>[
      kNonPositioned,
71
      TestParentData(top: 10.0, left: 10.0),
72 73 74
      kNonPositioned,
    ]);

75
    await tester.pumpWidget(
76
      Stack(
77
        textDirection: TextDirection.ltr,
78
        children: const <Widget>[
79
          Positioned(
80 81
            bottom: 5.0,
            right: 7.0,
82
            child: DecoratedBox(decoration: kBoxDecorationA),
83
          ),
84
          Positioned(
85 86
            top: 10.0,
            left: 10.0,
87
            child: DecoratedBox(decoration: kBoxDecorationB),
88
          ),
89
          DecoratedBox(decoration: kBoxDecorationC),
90 91
        ],
      ),
92 93 94
    );

    checkTree(tester, <TestParentData>[
95 96
      TestParentData(bottom: 5.0, right: 7.0),
      TestParentData(top: 10.0, left: 10.0),
97 98 99
      kNonPositioned,
    ]);

100 101 102
    const DecoratedBox kDecoratedBoxA = DecoratedBox(decoration: kBoxDecorationA);
    const DecoratedBox kDecoratedBoxB = DecoratedBox(decoration: kBoxDecorationB);
    const DecoratedBox kDecoratedBoxC = DecoratedBox(decoration: kBoxDecorationC);
103

104
    await tester.pumpWidget(
105
      Stack(
106
        textDirection: TextDirection.ltr,
107
        children: const <Widget>[
108
          Positioned(
109 110
            bottom: 5.0,
            right: 7.0,
111
            child: kDecoratedBoxA,
112
          ),
113
          Positioned(
114 115
            top: 10.0,
            left: 10.0,
116
            child: kDecoratedBoxB,
117 118
          ),
          kDecoratedBoxC,
119 120
        ],
      ),
121 122 123
    );

    checkTree(tester, <TestParentData>[
124 125
      TestParentData(bottom: 5.0, right: 7.0),
      TestParentData(top: 10.0, left: 10.0),
126 127 128
      kNonPositioned,
    ]);

129
    await tester.pumpWidget(
130
      Stack(
131
        textDirection: TextDirection.ltr,
132
        children: const <Widget>[
133
          Positioned(
134 135
            bottom: 6.0,
            right: 8.0,
136
            child: kDecoratedBoxA,
137
          ),
138
          Positioned(
139 140
            left: 10.0,
            right: 10.0,
141
            child: kDecoratedBoxB,
142 143
          ),
          kDecoratedBoxC,
144 145
        ],
      ),
146 147 148
    );

    checkTree(tester, <TestParentData>[
149 150
      TestParentData(bottom: 6.0, right: 8.0),
      TestParentData(left: 10.0, right: 10.0),
151 152 153
      kNonPositioned,
    ]);

154
    await tester.pumpWidget(
155
      Stack(
156
        textDirection: TextDirection.ltr,
157 158
        children: <Widget>[
          kDecoratedBoxA,
159
          Positioned(
160 161
            left: 11.0,
            right: 12.0,
162
            child: Container(child: kDecoratedBoxB),
163 164
          ),
          kDecoratedBoxC,
165 166
        ],
      ),
167 168 169 170
    );

    checkTree(tester, <TestParentData>[
      kNonPositioned,
171
      TestParentData(left: 11.0, right: 12.0),
172 173 174
      kNonPositioned,
    ]);

175
    await tester.pumpWidget(
176
      Stack(
177
        textDirection: TextDirection.ltr,
178 179
        children: <Widget>[
          kDecoratedBoxA,
180
          Positioned(
181
            right: 10.0,
182
            child: Container(child: kDecoratedBoxB),
183
          ),
184
          Container(
185
            child: const Positioned(
186
              top: 8.0,
187 188 189 190 191
              child: kDecoratedBoxC,
            ),
          ),
        ],
      ),
192 193 194 195
    );

    checkTree(tester, <TestParentData>[
      kNonPositioned,
196 197
      TestParentData(right: 10.0),
      TestParentData(top: 8.0),
198 199
    ]);

200
    await tester.pumpWidget(
201
      Stack(
202
        textDirection: TextDirection.ltr,
203
        children: const <Widget>[
204
          Positioned(
205
            right: 10.0,
206
            child: FlipWidget(left: kDecoratedBoxA, right: kDecoratedBoxB),
207
          ),
208 209
        ],
      ),
210 211 212
    );

    checkTree(tester, <TestParentData>[
213
      TestParentData(right: 10.0),
214 215 216
    ]);

    flipStatefulWidget(tester);
217
    await tester.pump();
218 219

    checkTree(tester, <TestParentData>[
220
      TestParentData(right: 10.0),
221 222
    ]);

223
    await tester.pumpWidget(
224
      Stack(
225
        textDirection: TextDirection.ltr,
226
        children: const <Widget>[
227
          Positioned(
228
            top: 7.0,
229
            child: FlipWidget(left: kDecoratedBoxA, right: kDecoratedBoxB),
230
          ),
231 232
        ],
      ),
233 234 235
    );

    checkTree(tester, <TestParentData>[
236
      TestParentData(top: 7.0),
237 238 239
    ]);

    flipStatefulWidget(tester);
240
    await tester.pump();
241 242

    checkTree(tester, <TestParentData>[
243
      TestParentData(top: 7.0),
244 245
    ]);

246
    await tester.pumpWidget(
247
      Stack(textDirection: TextDirection.ltr)
248 249 250
    );

    checkTree(tester, <TestParentData>[]);
Adam Barth's avatar
Adam Barth committed
251 252
  });

253 254
  testWidgets('ParentDataWidget conflicting data', (WidgetTester tester) async {
    await tester.pumpWidget(
255
      Directionality(
256
        textDirection: TextDirection.ltr,
257 258 259 260 261 262 263 264 265 266 267
        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),
              ),
268
            ),
269 270
          ],
        ),
271
      ),
272
    );
273

274 275 276 277 278 279
    dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(
      exception.toString(),
      equalsIgnoringHashCodes(
        'Incorrect use of ParentDataWidget.\n'
280 281 282 283 284 285 286 287
        '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]'
288 289
      ),
    );
290

291
    await tester.pumpWidget(Stack(textDirection: TextDirection.ltr));
292

293
    checkTree(tester, <TestParentData>[]);
294

295
    await tester.pumpWidget(
296 297 298 299 300 301 302 303 304 305 306 307
      Directionality(
        textDirection: TextDirection.ltr,
        child: Container(
          child: Row(
            children: const <Widget>[
              Positioned(
                top: 6.0,
                left: 7.0,
                child: DecoratedBox(decoration: kBoxDecorationB),
              ),
            ],
          ),
308 309
        ),
      ),
310
    );
311 312 313 314 315 316
    exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(
      exception.toString(),
      equalsIgnoringHashCodes(
        'Incorrect use of ParentDataWidget.\n'
317 318 319 320 321 322 323 324
        '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]'
325
      ),
326
    );
327

328
    await tester.pumpWidget(
329
      Stack(textDirection: TextDirection.ltr)
330
    );
331

332 333
    checkTree(tester, <TestParentData>[]);
  });
334

335
  testWidgets('ParentDataWidget interacts with global keys', (WidgetTester tester) async {
336
    final GlobalKey key = GlobalKey();
337

338
    await tester.pumpWidget(
339
      Stack(
340
        textDirection: TextDirection.ltr,
341
        children: <Widget>[
342
          Positioned(
343 344
            top: 10.0,
            left: 10.0,
345
            child: DecoratedBox(key: key, decoration: kBoxDecorationA),
346 347 348
          ),
        ],
      ),
349 350 351
    );

    checkTree(tester, <TestParentData>[
352
      TestParentData(top: 10.0, left: 10.0),
353 354
    ]);

355
    await tester.pumpWidget(
356
      Stack(
357
        textDirection: TextDirection.ltr,
358
        children: <Widget>[
359
          Positioned(
360 361
            top: 10.0,
            left: 10.0,
362
            child: DecoratedBox(
363
              decoration: kBoxDecorationB,
364
              child: DecoratedBox(key: key, decoration: kBoxDecorationA),
365 366 367 368
            ),
          ),
        ],
      ),
369 370 371
    );

    checkTree(tester, <TestParentData>[
372
      TestParentData(top: 10.0, left: 10.0),
373 374
    ]);

375
    await tester.pumpWidget(
376
      Stack(
377
        textDirection: TextDirection.ltr,
378
        children: <Widget>[
379
          Positioned(
380 381
            top: 10.0,
            left: 10.0,
382
            child: DecoratedBox(key: key, decoration: kBoxDecorationA),
383 384 385
          ),
        ],
      ),
386
    );
387

388
    checkTree(tester, <TestParentData>[
389
      TestParentData(top: 10.0, left: 10.0),
390
    ]);
391
  });
392 393

  testWidgets('Parent data invalid ancestor', (WidgetTester tester) async {
394 395 396 397 398 399 400 401 402 403 404 405 406 407
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Row(
        children: <Widget>[
          Stack(
            textDirection: TextDirection.ltr,
            children: <Widget>[
              Expanded(
                child: Container(),
              ),
            ],
          ),
        ],
      ),
408 409
    ));

410 411 412 413 414 415
    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    expect(
      exception.toString(),
      equalsIgnoringHashCodes(
        'Incorrect use of ParentDataWidget.\n'
416 417 418 419 420 421 422 423
        '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]'
424 425
      ),
    );
426
  });
427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

  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();
  }
Adam Barth's avatar
Adam Barth committed
519
}