reparent_state_harder_test.dart 5.68 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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 12 13 14 15
  const OrderSwitcher({
    Key? key,
    required this.a,
    required this.b,
  }) : super(key: key);
16 17 18 19 20

  final Widget a;
  final Widget b;

  @override
21
  OrderSwitcherState createState() => OrderSwitcherState();
22 23 24 25 26 27 28 29 30 31 32 33 34 35
}

class OrderSwitcherState extends State<OrderSwitcher> {

  bool _aFirst = true;

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

  @override
  Widget build(BuildContext context) {
36
    return Stack(
37
      textDirection: TextDirection.ltr,
38 39 40 41 42 43 44
      children: _aFirst
        ? <Widget>[
            KeyedSubtree(child: widget.a),
            widget.b,
          ]
        : <Widget>[
            KeyedSubtree(child: widget.b),
45
            widget.a,
46
          ],
47 48 49 50 51
    );
  }
}

class DummyStatefulWidget extends StatefulWidget {
52
  const DummyStatefulWidget(Key? key) : super(key: key);
53 54

  @override
55
  DummyStatefulWidgetState createState() => DummyStatefulWidgetState();
56 57 58 59
}

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

class RekeyableDummyStatefulWidgetWrapper extends StatefulWidget {
64
  const RekeyableDummyStatefulWidgetWrapper({
65
    Key? key,
66
    this.child,
67
    required this.initialKey,
68
  }) : super(key: key);
69
  final Widget? child;
70 71
  final GlobalKey initialKey;
  @override
72
  RekeyableDummyStatefulWidgetWrapperState createState() => RekeyableDummyStatefulWidgetWrapperState();
73 74 75
}

class RekeyableDummyStatefulWidgetWrapperState extends State<RekeyableDummyStatefulWidgetWrapper> {
76
  GlobalKey? _key;
77 78 79 80

  @override
  void initState() {
    super.initState();
81
    _key = widget.initialKey;
82 83
  }

84
  void _setChild(GlobalKey? value) {
85 86 87 88 89 90 91
    setState(() {
      _key = value;
    });
  }

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

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.

121 122 123 124 125 126
    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(
127
      key: keyRoot,
128
      a: KeyedSubtree(
129
        key: keyA,
130
        child: RekeyableDummyStatefulWidgetWrapper(
131 132 133
          initialKey: keyC
        ),
      ),
134
      b: KeyedSubtree(
135
        key: keyB,
136
        child: Builder(
137
          builder: (BuildContext context) {
138
            return Builder(
139
              builder: (BuildContext context) {
140
                return Builder(
141
                  builder: (BuildContext context) {
142
                    return LayoutBuilder(
143
                      builder: (BuildContext context, BoxConstraints constraints) {
144
                        return RekeyableDummyStatefulWidgetWrapper(
145 146 147 148 149 150 151 152 153
                          initialKey: keyD
                        );
                      }
                    );
                  }
                );
              }
            );
          }
154
        ),
155 156 157 158 159 160 161 162 163 164
      ),
    ));

    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));

165
    keyRoot.currentState!.switchChildren();
166
    final List<State> states = tester.stateList(find.byType(RekeyableDummyStatefulWidgetWrapper)).toList();
167
    final RekeyableDummyStatefulWidgetWrapperState a = states[0] as RekeyableDummyStatefulWidgetWrapperState;
168
    a._setChild(null);
169
    final RekeyableDummyStatefulWidgetWrapperState b = states[1] as RekeyableDummyStatefulWidgetWrapperState;
170
    b._setChild(keyC);
171 172 173 174 175 176 177 178 179 180
    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));
  });
}