blob: ba4c9e8a23e298b2d06422d5059fb379b4b2aeac [file] [log] [blame]
// 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
typedef ElementRebuildCallback = void Function(StatefulElement element);
class TestState extends State<StatefulWidget> {
@override
Widget build(BuildContext context) => const SizedBox();
}
@optionalTypeArgs
class _MyGlobalObjectKey<T extends State<StatefulWidget>> extends GlobalObjectKey<T> {
const _MyGlobalObjectKey(Object value) : super(value);
}
void main() {
testWidgets('UniqueKey control test', (WidgetTester tester) async {
final Key key = UniqueKey();
expect(key, hasOneLineDescription);
expect(key, isNot(equals(UniqueKey())));
});
testWidgets('ObjectKey control test', (WidgetTester tester) async {
final Object a = Object();
final Object b = Object();
final Key keyA = ObjectKey(a);
final Key keyA2 = ObjectKey(a);
final Key keyB = ObjectKey(b);
expect(keyA, hasOneLineDescription);
expect(keyA, equals(keyA2));
expect(keyA.hashCode, equals(keyA2.hashCode));
expect(keyA, isNot(equals(keyB)));
});
testWidgets('GlobalObjectKey toString test', (WidgetTester tester) async {
const GlobalObjectKey one = GlobalObjectKey(1);
const GlobalObjectKey<TestState> two = GlobalObjectKey<TestState>(2);
const GlobalObjectKey three = _MyGlobalObjectKey(3);
const GlobalObjectKey<TestState> four = _MyGlobalObjectKey<TestState>(4);
expect(one.toString(), equals('[GlobalObjectKey ${describeIdentity(1)}]'));
expect(two.toString(), equals('[GlobalObjectKey<TestState> ${describeIdentity(2)}]'));
expect(three.toString(), equals('[_MyGlobalObjectKey ${describeIdentity(3)}]'));
expect(four.toString(), equals('[_MyGlobalObjectKey<TestState> ${describeIdentity(4)}]'));
});
testWidgets('GlobalObjectKey control test', (WidgetTester tester) async {
final Object a = Object();
final Object b = Object();
final Key keyA = GlobalObjectKey(a);
final Key keyA2 = GlobalObjectKey(a);
final Key keyB = GlobalObjectKey(b);
expect(keyA, hasOneLineDescription);
expect(keyA, equals(keyA2));
expect(keyA.hashCode, equals(keyA2.hashCode));
expect(keyA, isNot(equals(keyB)));
});
testWidgets('GlobalKey correct case 1 - can move global key from container widget to layoutbuilder', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'correct');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return const Placeholder();
},
),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: const Placeholder(),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return SizedBox(key: key);
},
),
],
));
});
testWidgets('GlobalKey correct case 2 - can move global key from layoutbuilder to container widget', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'correct');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: const Placeholder(),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return SizedBox(key: key);
},
),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
LayoutBuilder(
key: const ValueKey<int>(2),
builder: (BuildContext context, BoxConstraints constraints) {
return const Placeholder();
},
),
],
));
});
testWidgets('GlobalKey correct case 3 - can deal with early rebuild in layoutbuilder - move backward', (WidgetTester tester) async {
const Key key1 = GlobalObjectKey('Text1');
const Key key2 = GlobalObjectKey('Text2');
Key? rebuiltKeyOfSecondChildBeforeLayout;
Key? rebuiltKeyOfFirstChildAfterLayout;
Key? rebuiltKeyOfSecondChildAfterLayout;
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildBeforeLayout =
statefulWidget.child.key;
},
),
],
);
},
),
);
// Result will be written during first build and need to clear it to remove
// noise.
rebuiltKeyOfSecondChildBeforeLayout = null;
final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1));
state.rebuild();
// Reorders the items
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfFirstChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfFirstChildAfterLayout = statefulWidget.child.key;
},
),
_Stateful(
child: const Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key;
},
),
],
);
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfFirstChildAfterLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key1);
});
testWidgets('GlobalKey correct case 4 - can deal with early rebuild in layoutbuilder - move forward', (WidgetTester tester) async {
const Key key1 = GlobalObjectKey('Text1');
const Key key2 = GlobalObjectKey('Text2');
const Key key3 = GlobalObjectKey('Text3');
Key? rebuiltKeyOfSecondChildBeforeLayout;
Key? rebuiltKeyOfSecondChildAfterLayout;
Key? rebuiltKeyOfThirdChildAfterLayout;
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key;
},
),
const _Stateful(
child: Text(
'Text3',
textDirection: TextDirection.ltr,
key: key3,
),
),
],
);
},
),
);
// Result will be written during first build and need to clear it to remove
// noise.
rebuiltKeyOfSecondChildBeforeLayout = null;
final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1));
state.rebuild();
// Reorders the items
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
),
_Stateful(
child: const Text(
'Text3',
textDirection: TextDirection.ltr,
key: key3,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key;
},
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfThirdChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfThirdChildAfterLayout = statefulWidget.child.key;
},
),
],
);
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key3);
expect(rebuiltKeyOfThirdChildAfterLayout, key2);
});
testWidgets('GlobalKey correct case 5 - can deal with early rebuild in layoutbuilder - only one global key', (WidgetTester tester) async {
const Key key1 = GlobalObjectKey('Text1');
Key? rebuiltKeyOfSecondChildBeforeLayout;
Key? rebuiltKeyOfThirdChildAfterLayout;
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
),
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key1,
),
onElementRebuild: (StatefulElement element) {
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key;
},
),
const _Stateful(
child: Text(
'Text3',
textDirection: TextDirection.ltr,
),
),
],
);
},
),
);
// Result will be written during first build and need to clear it to remove
// noise.
rebuiltKeyOfSecondChildBeforeLayout = null;
final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1));
state.rebuild();
// Reorders the items
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
),
),
_Stateful(
child: const Text(
'Text3',
textDirection: TextDirection.ltr,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
},
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key1,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
// We don't want noise to override the result;
expect(rebuiltKeyOfThirdChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfThirdChildAfterLayout = statefulWidget.child.key;
},
),
],
);
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, key1);
expect(rebuiltKeyOfThirdChildAfterLayout, key1);
});
testWidgets('GlobalKey duplication 1 - double appearance', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
Container(
key: const ValueKey<int>(2),
child: Placeholder(key: key),
),
],
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.',
),
);
});
testWidgets('GlobalKey duplication 2 - splitting and changing type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
),
Container(
key: const ValueKey<int>(2),
),
Container(
key: key,
),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(
key: const ValueKey<int>(1),
child: SizedBox(key: key),
),
Container(
key: const ValueKey<int>(2),
child: Placeholder(key: key),
),
],
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#00000 problematic] was used by multiple widgets. The parents of those widgets were:\n'
'- Container-[<1>]\n'
'- Container-[<2>]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.',
),
);
});
testWidgets('GlobalKey duplication 3 - splitting and changing type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
SizedBox(key: key),
Placeholder(key: key),
],
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose) has multiple children with key [GlobalKey#00000 problematic].'
),
);
});
testWidgets('GlobalKey duplication 4 - splitting and half changing type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Placeholder(key: key),
],
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose) has multiple children with key [GlobalKey#00000 problematic].'
),
);
});
testWidgets('GlobalKey duplication 5 - splitting and half changing type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Placeholder(key: key),
Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 6 - splitting and not changing type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 7 - appearing later', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: const ValueKey<int>(2)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: const ValueKey<int>(2), child: Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 8 - appearing earlier', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1)),
Container(key: const ValueKey<int>(2), child: Container(key: key)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: const ValueKey<int>(2), child: Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 9 - moving and appearing later', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0), child: Container(key: key)),
Container(key: const ValueKey<int>(1)),
Container(key: const ValueKey<int>(2)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: const ValueKey<int>(2), child: Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 10 - moving and appearing earlier', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1)),
Container(key: const ValueKey<int>(2)),
Container(key: const ValueKey<int>(3), child: Container(key: key)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: const ValueKey<int>(2), child: Container(key: key)),
Container(key: const ValueKey<int>(3)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 11 - double sibling appearance', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Container(key: key),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 12 - all kinds of badness at once', (WidgetTester tester) async {
final Key key1 = GlobalKey(debugLabel: 'problematic');
final Key key2 = GlobalKey(debugLabel: 'problematic'); // intentionally the same label
final Key key3 = GlobalKey(debugLabel: 'also problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key1),
Container(key: key1),
Row(
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key2),
Container(key: key2),
Container(key: key3),
Container(key: key2),
],
),
Row(
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key3),
],
),
Container(key: key3),
],
));
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate keys found.\n'
'If multiple keyed nodes exist as children of another node, they must have unique keys.\n'
'Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose) has multiple children with key [GlobalKey#00000 problematic].',
),
);
});
testWidgets('GlobalKey duplication 13 - all kinds of badness at once', (WidgetTester tester) async {
final Key key1 = GlobalKey(debugLabel: 'problematic');
final Key key2 = GlobalKey(debugLabel: 'problematic'); // intentionally the same label
final Key key3 = GlobalKey(debugLabel: 'also problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key1),
Container(key: key2),
Container(key: key3),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key1),
Container(key: key1),
Row(
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key2),
Container(key: key2),
Container(key: key2),
Container(key: key3),
Container(key: key2),
],
),
Row(
children: <Widget>[
Container(key: key1),
Container(key: key1),
Container(key: key3),
],
),
Container(key: key3),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 14 - moving during build - before', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1), child: Container(key: key)),
],
));
});
testWidgets('GlobalKey duplication 15 - duplicating during build - before', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: key),
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1), child: Container(key: key)),
],
));
expect(tester.takeException(), isFlutterError);
});
testWidgets('GlobalKey duplication 16 - moving during build - after', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
Container(key: key),
],
));
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1), child: Container(key: key)),
],
));
});
testWidgets('GlobalKey duplication 17 - duplicating during build - after', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
Container(key: key),
],
));
int count = 0;
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
expect(details.exception, isFlutterError);
count += 1;
};
await tester.pumpWidget(Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
Container(key: const ValueKey<int>(0)),
Container(key: const ValueKey<int>(1), child: Container(key: key)),
Container(key: key),
],
));
FlutterError.onError = oldHandler;
expect(count, 1);
});
testWidgets('GlobalKey duplication 18 - subtree build duplicate key with same type', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
final Stack stack = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
const SwapKeyWidget(childKey: ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
Container(key: key),
],
);
await tester.pumpWidget(stack);
final SwapKeyWidgetState state = tester.state(find.byType(SwapKeyWidget));
state.swapKey(key);
await tester.pump();
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Duplicate GlobalKey detected in widget tree.\n'
'The following GlobalKey was specified multiple times in the widget tree. This will lead '
'to parts of the widget tree being truncated unexpectedly, because the second time a key is seen, the '
'previous instance is moved to the new location. The key was:\n'
'- [GlobalKey#00000 problematic]\n'
'This was determined by noticing that after the widget with the above global key was '
'moved out of its previous parent, that previous parent never updated during this frame, meaning that '
'it either did not update at all or updated before the widget was moved, in either case implying that '
'it still thinks that it should have a child with that global key.\n'
'The specific parent that did not update after having one or more children forcibly '
'removed due to GlobalKey reparenting is:\n'
'- Stack(alignment: AlignmentDirectional.topStart, textDirection: ltr, fit: loose, '
'renderObject: RenderStack#00000)\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.',
),
);
});
testWidgets('GlobalKey duplication 19 - subtree build duplicate key with different types', (WidgetTester tester) async {
final Key key = GlobalKey(debugLabel: 'problematic');
final Stack stack = Stack(
textDirection: TextDirection.ltr,
children: <Widget>[
const SwapKeyWidget(childKey: ValueKey<int>(0)),
Container(key: const ValueKey<int>(1)),
Container(color: Colors.green, child: SizedBox(key: key)),
],
);
await tester.pumpWidget(stack);
final SwapKeyWidgetState state = tester.state(find.byType(SwapKeyWidget));
state.swapKey(key);
await tester.pump();
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalKey#95367 problematic] was used by 2 widgets:\n'
' SizedBox-[GlobalKey#00000 problematic]\n'
' Container-[GlobalKey#00000 problematic]\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.',
),
);
});
testWidgets('GlobalKey duplication 20 - real duplication with early rebuild in layoutbuilder will throw', (WidgetTester tester) async {
const Key key1 = GlobalObjectKey('Text1');
const Key key2 = GlobalObjectKey('Text2');
Key? rebuiltKeyOfSecondChildBeforeLayout;
Key? rebuiltKeyOfFirstChildAfterLayout;
Key? rebuiltKeyOfSecondChildAfterLayout;
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
const _Stateful(
child: Text(
'Text1',
textDirection: TextDirection.ltr,
key: key1,
),
),
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildBeforeLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildBeforeLayout = statefulWidget.child.key;
},
),
],
);
},
),
);
// Result will be written during first build and need to clear it to remove
// noise.
rebuiltKeyOfSecondChildBeforeLayout = null;
final _StatefulState state = tester.firstState(find.byType(_Stateful).at(1));
state.rebuild();
await tester.pumpWidget(
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: <Widget>[
_Stateful(
child: const Text(
'Text2',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfFirstChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfFirstChildAfterLayout = statefulWidget.child.key;
},
),
_Stateful(
child: const Text(
'Text1',
textDirection: TextDirection.ltr,
key: key2,
),
onElementRebuild: (StatefulElement element) {
// Verifies the early rebuild happens before layout.
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
// We don't want noise to override the result;
expect(rebuiltKeyOfSecondChildAfterLayout, isNull);
final _Stateful statefulWidget = element.widget as _Stateful;
rebuiltKeyOfSecondChildAfterLayout = statefulWidget.child.key;
},
),
],
);
},
),
);
expect(rebuiltKeyOfSecondChildBeforeLayout, key2);
expect(rebuiltKeyOfFirstChildAfterLayout, key2);
expect(rebuiltKeyOfSecondChildAfterLayout, key2);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'Multiple widgets used the same GlobalKey.\n'
'The key [GlobalObjectKey String#00000] was used by multiple widgets. The '
'parents of those widgets were:\n'
'- _Stateful(state: _StatefulState#00000)\n'
'- _Stateful(state: _StatefulState#00000)\n'
'A GlobalKey can only be specified on one widget at a time in the widget tree.',
),
);
});
testWidgets('GlobalKey - detach and re-attach child to different parents', (WidgetTester tester) async {
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
height: 100,
child: CustomScrollView(
controller: ScrollController(),
slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate(<Widget>[
Text('child', key: GlobalKey()),
]),
),
],
),
),
),
));
final SliverMultiBoxAdaptorElement element = tester.element(find.byType(SliverList));
late Element childElement;
// Removing and recreating child with same Global Key should not trigger
// duplicate key error.
element.visitChildren((Element e) {
childElement = e;
});
element.removeChild(childElement.renderObject! as RenderBox);
element.createChild(0, after: null);
element.visitChildren((Element e) {
childElement = e;
});
element.removeChild(childElement.renderObject! as RenderBox);
element.createChild(0, after: null);
});
testWidgets('GlobalKey - re-attach child to new parents, and the old parent is deactivated(unmounted)', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/62055
const Key key1 = GlobalObjectKey('key1');
const Key key2 = GlobalObjectKey('key2');
late StateSetter setState;
int tabBarViewCnt = 2;
TabController tabController = TabController(length: tabBarViewCnt, vsync: const TestVSync());
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return TabBarView(
controller: tabController,
children: <Widget>[
if (tabBarViewCnt > 0) const Text('key1', key: key1),
if (tabBarViewCnt > 1) const Text('key2', key: key2),
],
);
},
),
));
expect(tabController.index, 0);
// switch tabs 0 -> 1
setState(() {
tabController.index = 1;
});
await tester.pump(const Duration(seconds: 1)); // finish the animation
expect(tabController.index, 1);
// rebuild TabBarView that only have the 1st page with GlobalKey 'key1'
setState(() {
tabBarViewCnt = 1;
tabController = TabController(length: tabBarViewCnt, vsync: const TestVSync());
});
await tester.pump(const Duration(seconds: 1)); // finish the animation
expect(tabController.index, 0);
});
testWidgets('Defunct setState throws exception', (WidgetTester tester) async {
late StateSetter setState;
await tester.pumpWidget(StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Container();
},
));
// Control check that setState doesn't throw an exception.
setState(() { });
await tester.pumpWidget(Container());
expect(() { setState(() { }); }, throwsFlutterError);
});
testWidgets('State toString', (WidgetTester tester) async {
final TestState state = TestState();
expect(state.toString(), contains('no widget'));
});
testWidgets('debugPrintGlobalKeyedWidgetLifecycle control test', (WidgetTester tester) async {
expect(debugPrintGlobalKeyedWidgetLifecycle, isFalse);
final DebugPrintCallback oldCallback = debugPrint;
debugPrintGlobalKeyedWidgetLifecycle = true;
final List<String> log = <String>[];
debugPrint = (String? message, { int? wrapWidth }) {
log.add(message!);
};
final GlobalKey key = GlobalKey();
await tester.pumpWidget(Container(key: key));
expect(log, isEmpty);
await tester.pumpWidget(const Placeholder());
debugPrint = oldCallback;
debugPrintGlobalKeyedWidgetLifecycle = false;
expect(log.length, equals(2));
expect(log[0], matches('Deactivated'));
expect(log[1], matches('Discarding .+ from inactive elements list.'));
});
testWidgets('MultiChildRenderObjectElement.children', (WidgetTester tester) async {
GlobalKey key0, key1, key2;
await tester.pumpWidget(Column(
key: key0 = GlobalKey(),
children: <Widget>[
Container(),
Container(key: key1 = GlobalKey()),
Container(),
Container(key: key2 = GlobalKey()),
Container(),
],
));
final MultiChildRenderObjectElement element = key0.currentContext! as MultiChildRenderObjectElement;
expect(
element.children.map((Element element) => element.widget.key),
<Key?>[null, key1, null, key2, null],
);
});
testWidgets('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - mount', (WidgetTester tester) async {
await tester.pumpWidget(
Column(
children: <Widget>[
Container(),
const _EmptyWidget(),
],
),
);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n'
'This typically means that the `_EmptyWidget` or its children\n'
'are not a subtype of `RenderObjectWidget`.\n'
'The following element does not have an associated render object:\n'
' _EmptyWidget\n'
'debugCreator: _EmptyWidget ← Column ← [root]',
),
);
});
testWidgets('Can not attach a non-RenderObjectElement to the MultiChildRenderObjectElement - update', (WidgetTester tester) async {
await tester.pumpWidget(
Column(
children: <Widget>[
Container(),
],
),
);
await tester.pumpWidget(
Column(
children: <Widget>[
Container(),
const _EmptyWidget(),
],
),
);
final dynamic exception = tester.takeException();
expect(exception, isFlutterError);
expect(
exception.toString(),
equalsIgnoringHashCodes(
'The children of `MultiChildRenderObjectElement` must each has an associated render object.\n'
'This typically means that the `_EmptyWidget` or its children\n'
'are not a subtype of `RenderObjectWidget`.\n'
'The following element does not have an associated render object:\n'
' _EmptyWidget\n'
'debugCreator: _EmptyWidget ← Column ← [root]',
),
);
});
testWidgets('Element diagnostics', (WidgetTester tester) async {
GlobalKey key0;
await tester.pumpWidget(Column(
key: key0 = GlobalKey(),
children: <Widget>[
Container(),
Container(key: GlobalKey()),
Container(color: Colors.green, child: Container()),
Container(key: GlobalKey()),
Container(),
],
));
final MultiChildRenderObjectElement element = key0.currentContext! as MultiChildRenderObjectElement;
expect(element, hasAGoodToStringDeep);
expect(
element.toStringDeep(),
equalsIgnoringHashCodes(
'Column-[GlobalKey#00000](direction: vertical, mainAxisAlignment: start, crossAxisAlignment: center, renderObject: RenderFlex#00000)\n'
'├Container\n'
'│└LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up1)\n'
'│ └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up2)\n'
'├Container-[GlobalKey#00000]\n'
'│└LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up1)\n'
'│ └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up2)\n'
'├Container(bg: MaterialColor(primary value: Color(0xff4caf50)))\n'
'│└ColoredBox(color: MaterialColor(primary value: Color(0xff4caf50)), renderObject: _RenderColoredBox#00000 relayoutBoundary=up1)\n'
'│ └Container\n'
'│ └LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up2)\n'
'│ └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up3)\n'
'├Container-[GlobalKey#00000]\n'
'│└LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up1)\n'
'│ └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up2)\n'
'└Container\n'
' └LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#00000 relayoutBoundary=up1)\n'
' └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#00000 relayoutBoundary=up2)\n',
),
);
});
testWidgets('scheduleBuild while debugBuildingDirtyElements is true', (WidgetTester tester) async {
/// ignore here is required for testing purpose because changing the flag properly is hard
// ignore: invalid_use_of_protected_member
tester.binding.debugBuildingDirtyElements = true;
late FlutterError error;
try {
tester.binding.buildOwner!.scheduleBuildFor(
DirtyElementWithCustomBuildOwner(tester.binding.buildOwner!, Container()),
);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error.diagnostics.length, 3);
expect(error.diagnostics.last.level, DiagnosticLevel.hint);
expect(
error.diagnostics.last.toStringDeep(),
equalsIgnoringHashCodes(
'This might be because setState() was called from a layout or\n'
'paint callback. If a change is needed to the widget tree, it\n'
'should be applied as the tree is being built. Scheduling a change\n'
'for the subsequent frame instead results in an interface that\n'
'lags behind by one frame. If this was done to make your build\n'
'dependent on a size measured at layout time, consider using a\n'
'LayoutBuilder, CustomSingleChildLayout, or\n'
'CustomMultiChildLayout. If, on the other hand, the one frame\n'
'delay is the desired effect, for example because this is an\n'
'animation, consider scheduling the frame in a post-frame callback\n'
'using SchedulerBinding.addPostFrameCallback or using an\n'
'AnimationController to trigger the animation.\n',
),
);
expect(
error.toStringDeep(),
'FlutterError\n'
' Build scheduled during frame.\n'
' While the widget tree was being built, laid out, and painted, a\n'
' new frame was scheduled to rebuild the widget tree.\n'
' This might be because setState() was called from a layout or\n'
' paint callback. If a change is needed to the widget tree, it\n'
' should be applied as the tree is being built. Scheduling a change\n'
' for the subsequent frame instead results in an interface that\n'
' lags behind by one frame. If this was done to make your build\n'
' dependent on a size measured at layout time, consider using a\n'
' LayoutBuilder, CustomSingleChildLayout, or\n'
' CustomMultiChildLayout. If, on the other hand, the one frame\n'
' delay is the desired effect, for example because this is an\n'
' animation, consider scheduling the frame in a post-frame callback\n'
' using SchedulerBinding.addPostFrameCallback or using an\n'
' AnimationController to trigger the animation.\n',
);
}
});
testWidgets('didUpdateDependencies is not called on a State that never rebuilds', (WidgetTester tester) async {
final GlobalKey<DependentState> key = GlobalKey<DependentState>();
/// Initial build - should call didChangeDependencies, not deactivate
await tester.pumpWidget(Inherited(1, child: DependentStatefulWidget(key: key)));
final DependentState state = key.currentState!;
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 1);
expect(state.deactivatedCount, 0);
/// Rebuild with updated value - should call didChangeDependencies
await tester.pumpWidget(Inherited(2, child: DependentStatefulWidget(key: key)));
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 2);
expect(state.deactivatedCount, 0);
// reparent it - should call deactivate and didChangeDependencies
await tester.pumpWidget(Inherited(3, child: SizedBox(child: DependentStatefulWidget(key: key))));
expect(key.currentState, isNotNull);
expect(state.didChangeDependenciesCount, 3);
expect(state.deactivatedCount, 1);
// Remove it - should call deactivate, but not didChangeDependencies
await tester.pumpWidget(const Inherited(4, child: SizedBox()));
expect(key.currentState, isNull);
expect(state.didChangeDependenciesCount, 3);
expect(state.deactivatedCount, 2);
});
testWidgets('StatefulElement subclass can decorate State.build', (WidgetTester tester) async {
late bool isDidChangeDependenciesDecorated;
late bool isBuildDecorated;
final Widget child = Decorate(
didChangeDependencies: (bool value) {
isDidChangeDependenciesDecorated = value;
},
build: (bool value) {
isBuildDecorated = value;
},
);
await tester.pumpWidget(Inherited(0, child: child));
expect(isBuildDecorated, isTrue);
expect(isDidChangeDependenciesDecorated, isFalse);
await tester.pumpWidget(Inherited(1, child: child));
expect(isBuildDecorated, isTrue);
expect(isDidChangeDependenciesDecorated, isFalse);
});
group('BuildContext.debugDoingbuild', () {
testWidgets('StatelessWidget', (WidgetTester tester) async {
late bool debugDoingBuildOnBuild;
await tester.pumpWidget(
StatelessWidgetSpy(
onBuild: (BuildContext context) {
debugDoingBuildOnBuild = context.debugDoingBuild;
},
),
);
final Element context = tester.element(find.byType(StatelessWidgetSpy));
expect(context.debugDoingBuild, isFalse);
expect(debugDoingBuildOnBuild, isTrue);
});
testWidgets('StatefulWidget', (WidgetTester tester) async {
late bool debugDoingBuildOnBuild;
late bool debugDoingBuildOnInitState;
late bool debugDoingBuildOnDidChangeDependencies;
late bool debugDoingBuildOnDidUpdateWidget;
bool? debugDoingBuildOnDispose;
bool? debugDoingBuildOnDeactivate;
await tester.pumpWidget(
Inherited(
0,
child: StatefulWidgetSpy(
onInitState: (BuildContext context) {
debugDoingBuildOnInitState = context.debugDoingBuild;
},
onDidChangeDependencies: (BuildContext context) {
context.dependOnInheritedWidgetOfExactType<Inherited>();
debugDoingBuildOnDidChangeDependencies = context.debugDoingBuild;
},
onBuild: (BuildContext context) {
debugDoingBuildOnBuild = context.debugDoingBuild;
},
),
),
);
final Element context = tester.element(find.byType(StatefulWidgetSpy));
expect(context.debugDoingBuild, isFalse);
expect(debugDoingBuildOnBuild, isTrue);
expect(debugDoingBuildOnInitState, isFalse);
expect(debugDoingBuildOnDidChangeDependencies, isFalse);
await tester.pumpWidget(
Inherited(
1,
child: StatefulWidgetSpy(
onDidUpdateWidget: (BuildContext context) {
debugDoingBuildOnDidUpdateWidget = context.debugDoingBuild;
},
onDidChangeDependencies: (BuildContext context) {
debugDoingBuildOnDidChangeDependencies = context.debugDoingBuild;
},
onBuild: (BuildContext context) {
debugDoingBuildOnBuild = context.debugDoingBuild;
},
onDispose: (BuildContext context) {
debugDoingBuildOnDispose = context.debugDoingBuild;
},
onDeactivate: (BuildContext context) {
debugDoingBuildOnDeactivate = context.debugDoingBuild;
},
),
),
);
expect(context.debugDoingBuild, isFalse);
expect(debugDoingBuildOnBuild, isTrue);
expect(debugDoingBuildOnDidUpdateWidget, isFalse);
expect(debugDoingBuildOnDidChangeDependencies, isFalse);
expect(debugDoingBuildOnDeactivate, isNull);
expect(debugDoingBuildOnDispose, isNull);
await tester.pumpWidget(Container());
expect(context.debugDoingBuild, isFalse);
expect(debugDoingBuildOnDispose, isFalse);
expect(debugDoingBuildOnDeactivate, isFalse);
});
testWidgets('RenderObjectWidget', (WidgetTester tester) async {
late bool debugDoingBuildOnCreateRenderObject;
bool? debugDoingBuildOnUpdateRenderObject;
bool? debugDoingBuildOnDidUnmountRenderObject;
final ValueNotifier<int> notifier = ValueNotifier<int>(0);
late BuildContext spyContext;
Widget build() {
return ValueListenableBuilder<int>(
valueListenable: notifier,
builder: (BuildContext context, int? value, Widget? child) {
return Inherited(value, child: child!);
},
child: RenderObjectWidgetSpy(
onCreateRenderObject: (BuildContext context) {
spyContext = context;
context.dependOnInheritedWidgetOfExactType<Inherited>();
debugDoingBuildOnCreateRenderObject = context.debugDoingBuild;
},
onUpdateRenderObject: (BuildContext context) {
debugDoingBuildOnUpdateRenderObject = context.debugDoingBuild;
},
onDidUnmountRenderObject: () {
debugDoingBuildOnDidUnmountRenderObject = spyContext.debugDoingBuild;
},
),
);
}
await tester.pumpWidget(build());
spyContext = tester.element(find.byType(RenderObjectWidgetSpy));
expect(spyContext.debugDoingBuild, isFalse);
expect(debugDoingBuildOnCreateRenderObject, isTrue);
expect(debugDoingBuildOnUpdateRenderObject, isNull);
expect(debugDoingBuildOnDidUnmountRenderObject, isNull);
await tester.pumpWidget(build());
expect(spyContext.debugDoingBuild, isFalse);
expect(debugDoingBuildOnUpdateRenderObject, isTrue);
expect(debugDoingBuildOnDidUnmountRenderObject, isNull);
notifier.value++;
debugDoingBuildOnUpdateRenderObject = false;
await tester.pump();
expect(spyContext.debugDoingBuild, isFalse);
expect(debugDoingBuildOnUpdateRenderObject, isTrue);
expect(debugDoingBuildOnDidUnmountRenderObject, isNull);
await tester.pumpWidget(Container());
expect(spyContext.debugDoingBuild, isFalse);
expect(debugDoingBuildOnDidUnmountRenderObject, isFalse);
});
});
testWidgets('A widget whose element has an invalid visitChildren implementation triggers a useful error message', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(_WidgetWithNoVisitChildren(_StatefulLeaf(key: key)));
(key.currentState! as _StatefulLeafState).markNeedsBuild();
await tester.pumpWidget(Container());
final dynamic exception = tester.takeException();
expect(
// ignore: avoid_dynamic_calls
exception.message,
equalsIgnoringHashCodes(
'Tried to build dirty widget in the wrong build scope.\n'
'A widget which was marked as dirty and is still active was scheduled to be built, '
'but the current build scope unexpectedly does not contain that widget.\n'
'Sometimes this is detected when an element is removed from the widget tree, but '
'the element somehow did not get marked as inactive. In that case, it might be '
'caused by an ancestor element failing to implement visitChildren correctly, thus '
'preventing some or all of its descendants from being correctly deactivated.\n'
'The root of the build scope was:\n'
' [root]\n'
'The offending element (which does not appear to be a descendant of the root of '
'the build scope) was:\n'
' _StatefulLeaf-[GlobalKey#00000]',
),
);
});
testWidgets('Can create BuildOwner that does not interfere with pointer router or raw key event handler', (WidgetTester tester) async {
final int pointerRouterCount = GestureBinding.instance!.pointerRouter.debugGlobalRouteCount;
final RawKeyEventHandler? rawKeyEventHandler = RawKeyboard.instance.keyEventHandler;
expect(rawKeyEventHandler, isNotNull);
BuildOwner(focusManager: FocusManager());
expect(GestureBinding.instance!.pointerRouter.debugGlobalRouteCount, pointerRouterCount);
expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler));
});
testWidgets('Can access debugFillProperties without _LateInitializationError', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
TestRenderObjectElement().debugFillProperties(builder);
expect(builder.properties.any((DiagnosticsNode property) => property.name == 'renderObject' && property.value == null), isTrue);
});
testWidgets('BuildOwner.globalKeyCount keeps track of in-use global keys', (WidgetTester tester) async {
final int initialCount = tester.binding.buildOwner!.globalKeyCount;
final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey();
await tester.pumpWidget(Container(key: key1));
expect(tester.binding.buildOwner!.globalKeyCount, initialCount + 1);
await tester.pumpWidget(Container(key: key1, child: Container()));
expect(tester.binding.buildOwner!.globalKeyCount, initialCount + 1);
await tester.pumpWidget(Container(key: key1, child: Container(key: key2)));
expect(tester.binding.buildOwner!.globalKeyCount, initialCount + 2);
await tester.pumpWidget(Container());
expect(tester.binding.buildOwner!.globalKeyCount, initialCount + 0);
});
testWidgets('Widget and State properties are nulled out when unmounted', (WidgetTester tester) async {
await tester.pumpWidget(const _StatefulLeaf());
final StatefulElement element = tester.element<StatefulElement>(find.byType(_StatefulLeaf));
expect(element.state, isA<State<_StatefulLeaf>>());
expect(element.widget, isA<_StatefulLeaf>());
// Replace the widget tree to unmount the element.
await tester.pumpWidget(Container());
// Accessing state/widget now throws a CastError because they have been
// nulled out to reduce severity of memory leaks when an Element (e.g. in
// the form of a BuildContext) is retained past its useful life. See also
// https://github.com/flutter/flutter/issues/79605 for examples why this may
// occur.
expect(() => element.state, throwsA(isA<TypeError>()));
expect(() => element.widget, throwsA(isA<TypeError>()));
});
testWidgets('Deactivate and activate are called correctly', (WidgetTester tester) async {
final List<String> states = <String>[];
Widget build([Key? key]) {
return StatefulWidgetSpy(
key: key,
onInitState: (BuildContext context) { states.add('initState'); },
onDidUpdateWidget: (BuildContext context) { states.add('didUpdateWidget'); },
onDeactivate: (BuildContext context) { states.add('deactivate'); },
onActivate: (BuildContext context) { states.add('activate'); },
onBuild: (BuildContext context) { states.add('build'); },
onDispose: (BuildContext context) { states.add('dispose'); },
);
}
Future<void> pumpWidget(Widget widget) {
states.clear();
return tester.pumpWidget(widget);
}
await pumpWidget(build());
expect(states, <String>['initState', 'build']);
await pumpWidget(Container(child: build()));
expect(states, <String>['deactivate', 'initState', 'build', 'dispose']);
await pumpWidget(Container());
expect(states, <String>['deactivate', 'dispose']);
final GlobalKey key = GlobalKey();
await pumpWidget(build(key));
expect(states, <String>['initState', 'build']);
await pumpWidget(Container(child: build(key)));
expect(states, <String>['deactivate', 'activate', 'didUpdateWidget', 'build']);
await pumpWidget(Container());
expect(states, <String>['deactivate', 'dispose']);
});
testWidgets('RenderObjectElement.unmount disposes of its renderObject', (WidgetTester tester) async {
await tester.pumpWidget(const Placeholder());
final RenderObjectElement element = tester.allElements.whereType<RenderObjectElement>().first;
final RenderObject renderObject = element.renderObject;
expect(renderObject.debugDisposed, false);
await tester.pumpWidget(Container());
expect(() => element.renderObject, throwsAssertionError);
expect(renderObject.debugDisposed, true);
});
testWidgets('Getting the render object of an unmounted element throws', (WidgetTester tester) async {
await tester.pumpWidget(const _StatefulLeaf());
final StatefulElement element = tester.element<StatefulElement>(find.byType(_StatefulLeaf));
expect(element.state, isA<State<_StatefulLeaf>>());
expect(element.widget, isA<_StatefulLeaf>());
// Replace the widget tree to unmount the element.
await tester.pumpWidget(Container());
expect(
() => element.findRenderObject(),
throwsA(isA<FlutterError>().having(
(FlutterError error) => error.message,
'message',
equalsIgnoringHashCodes('''
Cannot get renderObject of inactive element.
In order for an element to have a valid renderObject, it must be active, which means it is part of the tree.
Instead, this element is in the _ElementLifecycle.defunct state.
If you called this method from a State object, consider guarding it with State.mounted.
The findRenderObject() method was called for the following element:
StatefulElement#00000(DEFUNCT)'''),
)),
);
});
}
class _WidgetWithNoVisitChildren extends StatelessWidget {
const _WidgetWithNoVisitChildren(this.child, { Key? key }) :
super(key: key);
final Widget child;
@override
Widget build(BuildContext context) => child;
@override
_WidgetWithNoVisitChildrenElement createElement() => _WidgetWithNoVisitChildrenElement(this);
}
class _WidgetWithNoVisitChildrenElement extends StatelessElement {
_WidgetWithNoVisitChildrenElement(_WidgetWithNoVisitChildren widget): super(widget);
@override
void visitChildren(ElementVisitor visitor) {
// This implementation is intentionally buggy, to test that an error message is
// shown when this situation occurs.
// The superclass has the correct implementation (calling `visitor(_child)`), so
// we don't call it here.
}
}
class _StatefulLeaf extends StatefulWidget {
const _StatefulLeaf({ Key? key }) : super(key: key);
@override
State<_StatefulLeaf> createState() => _StatefulLeafState();
}
class _StatefulLeafState extends State<_StatefulLeaf> {
void markNeedsBuild() {
setState(() { });
}
@override
Widget build(BuildContext context) {
return const SizedBox.shrink();
}
}
class Decorate extends StatefulWidget {
const Decorate({
Key? key,
required this.didChangeDependencies,
required this.build,
}) :
assert(didChangeDependencies != null),
assert(build != null),
super(key: key);
final void Function(bool isInBuild) didChangeDependencies;
final void Function(bool isInBuild) build;
@override
State<Decorate> createState() => _DecorateState();
@override
DecorateElement createElement() => DecorateElement(this);
}
class DecorateElement extends StatefulElement {
DecorateElement(Decorate widget): super(widget);
bool isDecorated = false;
@override
Widget build() {
try {
isDecorated = true;
return super.build();
} finally {
isDecorated = false;
}
}
}
class _DecorateState extends State<Decorate> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.didChangeDependencies.call((context as DecorateElement).isDecorated);
}
@override
Widget build(covariant DecorateElement context) {
context.dependOnInheritedWidgetOfExactType<Inherited>();
widget.build.call(context.isDecorated);
return Container();
}
}
class DirtyElementWithCustomBuildOwner extends Element {
DirtyElementWithCustomBuildOwner(BuildOwner buildOwner, Widget widget)
: _owner = buildOwner, super(widget);
final BuildOwner _owner;
@override
void performRebuild() {}
@override
BuildOwner get owner => _owner;
@override
bool get dirty => true;
@override
bool get debugDoingBuild => throw UnimplementedError();
}
class Inherited extends InheritedWidget {
const Inherited(this.value, {Key? key, required Widget child}) : super(key: key, child: child);
final int? value;
@override
bool updateShouldNotify(Inherited oldWidget) => oldWidget.value != value;
}
class DependentStatefulWidget extends StatefulWidget {
const DependentStatefulWidget({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => DependentState();
}
class DependentState extends State<DependentStatefulWidget> {
int didChangeDependenciesCount = 0;
int deactivatedCount = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
didChangeDependenciesCount += 1;
}
@override
Widget build(BuildContext context) {
context.dependOnInheritedWidgetOfExactType<Inherited>();
return const SizedBox();
}
@override
void deactivate() {
super.deactivate();
deactivatedCount += 1;
}
}
class SwapKeyWidget extends StatefulWidget {
const SwapKeyWidget({Key? key, this.childKey}): super(key: key);
final Key? childKey;
@override
SwapKeyWidgetState createState() => SwapKeyWidgetState();
}
class SwapKeyWidgetState extends State<SwapKeyWidget> {
Key? key;
@override
void initState() {
super.initState();
key = widget.childKey;
}
void swapKey(Key newKey) {
setState(() {
key = newKey;
});
}
@override
Widget build(BuildContext context) {
return Container(key: key);
}
}
class _Stateful extends StatefulWidget {
const _Stateful({Key? key, required this.child, this.onElementRebuild}) : super(key: key);
final Text child;
final ElementRebuildCallback? onElementRebuild;
@override
State<StatefulWidget> createState() => _StatefulState();
@override
StatefulElement createElement() => StatefulElementSpy(this);
}
class _StatefulState extends State<_Stateful> {
void rebuild() => setState(() {});
@override
Widget build(BuildContext context) {
return widget.child;
}
}
class StatefulElementSpy extends StatefulElement {
StatefulElementSpy(StatefulWidget widget) : super(widget);
_Stateful get _statefulWidget => widget as _Stateful;
@override
void rebuild() {
_statefulWidget.onElementRebuild?.call(this);
super.rebuild();
}
}
class StatelessWidgetSpy extends StatelessWidget {
const StatelessWidgetSpy({
Key? key,
required this.onBuild,
}) : assert(onBuild != null),
super(key: key);
final void Function(BuildContext) onBuild;
@override
Widget build(BuildContext context) {
onBuild(context);
return Container();
}
}
class StatefulWidgetSpy extends StatefulWidget {
const StatefulWidgetSpy({
Key? key,
this.onBuild,
this.onInitState,
this.onDidChangeDependencies,
this.onDispose,
this.onDeactivate,
this.onActivate,
this.onDidUpdateWidget,
}) : super(key: key);
final void Function(BuildContext)? onBuild;
final void Function(BuildContext)? onInitState;
final void Function(BuildContext)? onDidChangeDependencies;
final void Function(BuildContext)? onDispose;
final void Function(BuildContext)? onDeactivate;
final void Function(BuildContext)? onActivate;
final void Function(BuildContext)? onDidUpdateWidget;
@override
State<StatefulWidgetSpy> createState() => _StatefulWidgetSpyState();
}
class _StatefulWidgetSpyState extends State<StatefulWidgetSpy> {
@override
void initState() {
super.initState();
widget.onInitState?.call(context);
}
@override
void deactivate() {
super.deactivate();
widget.onDeactivate?.call(context);
}
@override
void activate() {
super.activate();
widget.onActivate?.call(context);
}
@override
void dispose() {
super.dispose();
widget.onDispose?.call(context);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onDidChangeDependencies?.call(context);
}
@override
void didUpdateWidget(StatefulWidgetSpy oldWidget) {
super.didUpdateWidget(oldWidget);
widget.onDidUpdateWidget?.call(context);
}
@override
Widget build(BuildContext context) {
widget.onBuild?.call(context);
return Container();
}
}
class RenderObjectWidgetSpy extends LeafRenderObjectWidget {
const RenderObjectWidgetSpy({
Key? key,
this.onCreateRenderObject,
this.onUpdateRenderObject,
this.onDidUnmountRenderObject,
}) : super(key: key);
final void Function(BuildContext)? onCreateRenderObject;
final void Function(BuildContext)? onUpdateRenderObject;
final void Function()? onDidUnmountRenderObject;
@override
RenderObject createRenderObject(BuildContext context) {
onCreateRenderObject?.call(context);
return FakeLeafRenderObject();
}
@override
void updateRenderObject(BuildContext context, RenderObject renderObject) {
onUpdateRenderObject?.call(context);
}
@override
void didUnmountRenderObject(RenderObject renderObject) {
super.didUnmountRenderObject(renderObject);
onDidUnmountRenderObject?.call();
}
}
class FakeLeafRenderObject extends RenderBox {
@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void performLayout() {
size = constraints.biggest;
}
}
class TestRenderObjectElement extends RenderObjectElement {
TestRenderObjectElement() : super(Table());
}
class _EmptyWidget extends Widget {
const _EmptyWidget({Key? key}) : super(key: key);
@override
Element createElement() => _EmptyElement(this);
}
class _EmptyElement extends Element {
_EmptyElement(_EmptyWidget widget) : super(widget);
@override
bool get debugDoingBuild => false;
@override
void performRebuild() {}
}