reparent_state_harder_test.dart 5.56 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
import 'package:flutter_test/flutter_test.dart';
7 8 9 10

// 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

  final Widget a;
  final Widget b;

  @override
17
  OrderSwitcherState createState() => OrderSwitcherState();
18 19 20 21 22 23 24 25 26 27 28 29 30 31
}

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
      children.add(KeyedSubtree(child: widget.a));
35
      children.add(widget.b);
36
    } else {
37
      children.add(KeyedSubtree(child: widget.b));
38
      children.add(widget.a);
39
    }
40
    return Stack(
41 42
      textDirection: TextDirection.ltr,
      children: children,
43 44 45 46 47
    );
  }
}

class DummyStatefulWidget extends StatefulWidget {
48
  const DummyStatefulWidget(Key key) : super(key: key);
49 50

  @override
51
  DummyStatefulWidgetState createState() => DummyStatefulWidgetState();
52 53 54 55
}

class DummyStatefulWidgetState extends State<DummyStatefulWidget> {
  @override
Ian Hickson's avatar
Ian Hickson committed
56
  Widget build(BuildContext context) => const Text('LEAF', textDirection: TextDirection.ltr);
57 58 59
}

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

class RekeyableDummyStatefulWidgetWrapperState extends State<RekeyableDummyStatefulWidgetWrapper> {
  GlobalKey _key;

  @override
  void initState() {
    super.initState();
73
    _key = widget.initialKey;
74 75 76 77 78 79 80 81 82 83
  }

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

  @override
  Widget build(BuildContext context) {
84
    return DummyStatefulWidget(_key);
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
  }
}

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.

113 114 115 116 117 118
    final GlobalKey<OrderSwitcherState> keyRoot = GlobalKey(debugLabel: 'Root');
    final GlobalKey keyA = GlobalKey(debugLabel: 'A');
    final GlobalKey keyB = GlobalKey(debugLabel: 'B');
    final GlobalKey keyC = GlobalKey(debugLabel: 'C');
    final GlobalKey keyD = GlobalKey(debugLabel: 'D');
    await tester.pumpWidget(OrderSwitcher(
119
      key: keyRoot,
120
      a: KeyedSubtree(
121
        key: keyA,
122
        child: RekeyableDummyStatefulWidgetWrapper(
123 124 125
          initialKey: keyC
        ),
      ),
126
      b: KeyedSubtree(
127
        key: keyB,
128
        child: Builder(
129
          builder: (BuildContext context) {
130
            return Builder(
131
              builder: (BuildContext context) {
132
                return Builder(
133
                  builder: (BuildContext context) {
134
                    return LayoutBuilder(
135
                      builder: (BuildContext context, BoxConstraints constraints) {
136
                        return RekeyableDummyStatefulWidgetWrapper(
137 138 139 140 141 142 143 144 145
                          initialKey: keyD
                        );
                      }
                    );
                  }
                );
              }
            );
          }
146
        ),
147 148 149 150 151 152 153 154 155 156 157
      ),
    ));

    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();
158
    final List<State> states = tester.stateList(find.byType(RekeyableDummyStatefulWidgetWrapper)).toList();
159 160 161 162
    final RekeyableDummyStatefulWidgetWrapperState a = states[0];
    a._setChild(null);
    final RekeyableDummyStatefulWidgetWrapperState b = states[1];
    b._setChild(keyC);
163 164 165 166 167 168 169 170 171 172
    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));
  });
}