reparent_state_harder_test.dart 5.57 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/widgets.dart';
6 7 8 9 10
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;

// This is a regression test for https://github.com/flutter/flutter/issues/5588.

class OrderSwitcher extends StatefulWidget {
11
  const OrderSwitcher({ Key key, this.a, this.b }) : super(key: key);
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

  final Widget a;
  final Widget b;

  @override
  OrderSwitcherState createState() => new OrderSwitcherState();
}

class OrderSwitcherState extends State<OrderSwitcher> {

  bool _aFirst = true;

  void switchChildren() {
    setState(() {
      _aFirst = false;
    });
  }

  @override
  Widget build(BuildContext context) {
32
    final List<Widget> children = <Widget>[];
33
    if (_aFirst) {
34 35
      children.add(new KeyedSubtree(child: widget.a));
      children.add(widget.b);
36
    } else {
37 38
      children.add(new KeyedSubtree(child: widget.b));
      children.add(widget.a);
39 40 41 42 43 44 45 46
    }
    return new Stack(
      children: children
    );
  }
}

class DummyStatefulWidget extends StatefulWidget {
47
  const DummyStatefulWidget(Key key) : super(key: key);
48 49 50 51 52 53 54

  @override
  DummyStatefulWidgetState createState() => new DummyStatefulWidgetState();
}

class DummyStatefulWidgetState extends State<DummyStatefulWidget> {
  @override
55
  Widget build(BuildContext context) => const Text('LEAF');
56 57 58
}

class RekeyableDummyStatefulWidgetWrapper extends StatefulWidget {
59
  const RekeyableDummyStatefulWidgetWrapper({ this.child, this.initialKey });
60 61 62 63 64 65 66 67 68 69 70 71
  final Widget child;
  final GlobalKey initialKey;
  @override
  RekeyableDummyStatefulWidgetWrapperState createState() => new RekeyableDummyStatefulWidgetWrapperState();
}

class RekeyableDummyStatefulWidgetWrapperState extends State<RekeyableDummyStatefulWidgetWrapper> {
  GlobalKey _key;

  @override
  void initState() {
    super.initState();
72
    _key = widget.initialKey;
73 74 75 76 77 78 79 80 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
  }

  void _setChild(GlobalKey value) {
    setState(() {
      _key = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new DummyStatefulWidget(_key);
  }
}

void main() {
  testWidgets('Handle GlobalKey reparenting in weird orders', (WidgetTester tester) async {

    // This is a bit of a weird test so let's try to explain it a bit.
    //
    // Basically what's happening here is that we have a complicated tree, and
    // in one frame, we change it to a slightly different tree with a specific
    // set of mutations:
    //
    // * The keyA subtree is regrafted to be one level higher, but later than
    //   the keyB subtree.
    // * The keyB subtree is, similarly, moved one level deeper, but earlier, than
    //   the keyA subtree.
    // * The keyD subtree is replaced by the previously earlier and shallower
    //   keyC subtree. This happens during a LayoutBuilder layout callback, so it
    //   happens long after A and B have finished their dance.
    //
    // The net result is that when keyC is moved, it has already been marked
    // dirty from being removed then reinserted into the tree (redundantly, as
    // it turns out, though this isn't known at the time), and has already been
    // visited once by the code that tries to clean nodes (though at that point
    // nothing happens since it isn't in the tree).
    //
    // This test verifies that none of the asserts go off during this dance.

    final GlobalKey<OrderSwitcherState> keyRoot = new GlobalKey(debugLabel: 'Root');
    final GlobalKey keyA = new GlobalKey(debugLabel: 'A');
    final GlobalKey keyB = new GlobalKey(debugLabel: 'B');
    final GlobalKey keyC = new GlobalKey(debugLabel: 'C');
    final GlobalKey keyD = new GlobalKey(debugLabel: 'D');
    await tester.pumpWidget(new OrderSwitcher(
      key: keyRoot,
      a: new KeyedSubtree(
        key: keyA,
        child: new RekeyableDummyStatefulWidgetWrapper(
          initialKey: keyC
        ),
      ),
      b: new KeyedSubtree(
        key: keyB,
        child: new Builder(
          builder: (BuildContext context) {
            return new Builder(
              builder: (BuildContext context) {
                return new Builder(
                  builder: (BuildContext context) {
                    return new LayoutBuilder(
                      builder: (BuildContext context, BoxConstraints constraints) {
                        return new RekeyableDummyStatefulWidgetWrapper(
                          initialKey: keyD
                        );
                      }
                    );
                  }
                );
              }
            );
          }
        )
      ),
    ));

    expect(find.byKey(keyA), findsOneWidget);
    expect(find.byKey(keyB), findsOneWidget);
    expect(find.byKey(keyC), findsOneWidget);
    expect(find.byKey(keyD), findsOneWidget);
    expect(find.byType(RekeyableDummyStatefulWidgetWrapper), findsNWidgets(2));
    expect(find.byType(DummyStatefulWidget), findsNWidgets(2));

    keyRoot.currentState.switchChildren();
157 158 159
    final List<State> states = tester.stateList(find.byType(RekeyableDummyStatefulWidgetWrapper)).toList();
    final RekeyableDummyStatefulWidgetWrapperState a = states[0]; a._setChild(null);
    final RekeyableDummyStatefulWidgetWrapperState b = states[1]; b._setChild(keyC);
160 161 162 163 164 165 166 167 168 169
    await tester.pump();

    expect(find.byKey(keyA), findsOneWidget);
    expect(find.byKey(keyB), findsOneWidget);
    expect(find.byKey(keyC), findsOneWidget);
    expect(find.byKey(keyD), findsNothing);
    expect(find.byType(RekeyableDummyStatefulWidgetWrapper), findsNWidgets(2));
    expect(find.byType(DummyStatefulWidget), findsNWidgets(2));
  });
}