| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| // This is a regression test for https://github.com/flutter/flutter/issues/5588. |
| |
| class OrderSwitcher extends StatefulWidget { |
| const OrderSwitcher({ |
| Key? key, |
| required this.a, |
| required this.b, |
| }) : super(key: key); |
| |
| final Widget a; |
| final Widget b; |
| |
| @override |
| OrderSwitcherState createState() => OrderSwitcherState(); |
| } |
| |
| class OrderSwitcherState extends State<OrderSwitcher> { |
| |
| bool _aFirst = true; |
| |
| void switchChildren() { |
| setState(() { |
| _aFirst = false; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Stack( |
| textDirection: TextDirection.ltr, |
| children: _aFirst |
| ? <Widget>[ |
| KeyedSubtree(child: widget.a), |
| widget.b, |
| ] |
| : <Widget>[ |
| KeyedSubtree(child: widget.b), |
| widget.a, |
| ], |
| ); |
| } |
| } |
| |
| class DummyStatefulWidget extends StatefulWidget { |
| const DummyStatefulWidget(Key? key) : super(key: key); |
| |
| @override |
| DummyStatefulWidgetState createState() => DummyStatefulWidgetState(); |
| } |
| |
| class DummyStatefulWidgetState extends State<DummyStatefulWidget> { |
| @override |
| Widget build(BuildContext context) => const Text('LEAF', textDirection: TextDirection.ltr); |
| } |
| |
| class RekeyableDummyStatefulWidgetWrapper extends StatefulWidget { |
| const RekeyableDummyStatefulWidgetWrapper({ |
| Key? key, |
| this.child, |
| required this.initialKey, |
| }) : super(key: key); |
| final Widget? child; |
| final GlobalKey initialKey; |
| @override |
| RekeyableDummyStatefulWidgetWrapperState createState() => RekeyableDummyStatefulWidgetWrapperState(); |
| } |
| |
| class RekeyableDummyStatefulWidgetWrapperState extends State<RekeyableDummyStatefulWidgetWrapper> { |
| GlobalKey? _key; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _key = widget.initialKey; |
| } |
| |
| void _setChild(GlobalKey? value) { |
| setState(() { |
| _key = value; |
| }); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return 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 = 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( |
| key: keyRoot, |
| a: KeyedSubtree( |
| key: keyA, |
| child: RekeyableDummyStatefulWidgetWrapper( |
| initialKey: keyC, |
| ), |
| ), |
| b: KeyedSubtree( |
| key: keyB, |
| child: Builder( |
| builder: (BuildContext context) { |
| return Builder( |
| builder: (BuildContext context) { |
| return Builder( |
| builder: (BuildContext context) { |
| return LayoutBuilder( |
| builder: (BuildContext context, BoxConstraints constraints) { |
| return 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(); |
| final List<State> states = tester.stateList(find.byType(RekeyableDummyStatefulWidgetWrapper)).toList(); |
| final RekeyableDummyStatefulWidgetWrapperState a = states[0] as RekeyableDummyStatefulWidgetWrapperState; |
| a._setChild(null); |
| final RekeyableDummyStatefulWidgetWrapperState b = states[1] as RekeyableDummyStatefulWidgetWrapperState; |
| b._setChild(keyC); |
| 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)); |
| }); |
| } |