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

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

class OrderSwitcher extends StatefulWidget {
12
  const OrderSwitcher({
13
    super.key,
14 15
    required this.a,
    required this.b,
16
  });
17 18 19 20 21

  final Widget a;
  final Widget b;

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

class OrderSwitcherState extends State<OrderSwitcher> {

  bool _aFirst = true;

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

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

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

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

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

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

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

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

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

  @override
  Widget build(BuildContext context) {
91
    return DummyStatefulWidget(_key);
92 93 94 95
  }
}

void main() {
96
  testWidgetsWithLeakTracking('Handle GlobalKey reparenting in weird orders', (WidgetTester tester) async {
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119

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

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

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

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