mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This is for #172289, but since this fix is speculative so I'll wait for the confirmation from the original reporters before closing the issue. As a bonus this fixes #65655 The framework Element rebuild logic relies heavily on `Element._lifecycleState` being correct. When a user-supplied lifecycle callback (e.g., `State.deactivate`) fails the framework currently only ensures that every `Element` in the tree has the right lifecycle state, so an out-of-tree `Element` that is supposed to be disposed may still have an `active` state and continue being rebuilt by the BuildScope (because it's in the dirty list). See the comments in #172289 Also related: #100777 Internal: b/425298525 b/431537277 b/300829376 b/415724119 b/283614822 # TODO (in a different PR) The original issue could also be caused by incorrect `Element.updateChild` calls. If an `Element` subclass calls `Element.updateChild` to add child but forgets to update its child list accordingly (such that `visitChildren` misses that child), you'll get a child Element that thinks it's a child of the parent but the parent doesn't recognize the child so won't take that child into account during reparenting or unmounting. This is a programmer error that we should try to catch in the framework. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
966 lines
29 KiB
Dart
966 lines
29 KiB
Dart
// 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 'dart:ui';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
|
|
|
import 'multi_view_testing.dart';
|
|
|
|
void main() {
|
|
testWidgets('Providing a RenderObjectWidget directly to the RootWidget fails', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// No render tree exists to attach the RenderObjectWidget to.
|
|
await tester.pumpWidget(wrapWithView: false, const ColoredBox(color: Colors.red));
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith(
|
|
'The render object for ColoredBox cannot find ancestor render object to attach to.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Moving a RenderObjectWidget to the RootWidget via GlobalKey fails', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Widget globalKeyedWidget = ColoredBox(key: GlobalKey(), color: Colors.red);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, View(view: tester.view, child: globalKeyedWidget));
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, globalKeyedWidget);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot find ancestor render object to attach to.'),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'A View cannot be a child of a render object widget',
|
|
experimentalLeakTesting: LeakTesting.settings
|
|
.withIgnoredAll(), // leaking by design because of exception
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Center(
|
|
child: View(view: FakeView(tester.view), child: Container()),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot maintain an independent render tree at its current location.'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'The child of a ViewAnchor cannot be a View',
|
|
experimentalLeakTesting: LeakTesting.settings
|
|
.withIgnoredAll(), // leaking by design because of exception
|
|
(WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
ViewAnchor(
|
|
child: View(view: FakeView(tester.view), child: Container()),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot maintain an independent render tree at its current location.'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets(
|
|
'A View can not be moved via GlobalKey to be a child of a RenderObject',
|
|
experimentalLeakTesting: LeakTesting.settings
|
|
.withIgnoredAll(), // leaking by design because of exception
|
|
(WidgetTester tester) async {
|
|
final Widget globalKeyedView = View(
|
|
key: GlobalKey(),
|
|
view: FakeView(tester.view),
|
|
child: const ColoredBox(color: Colors.red),
|
|
);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, globalKeyedView);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, View(view: tester.view, child: globalKeyedView));
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot maintain an independent render tree at its current location.'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('The view property of a ViewAnchor cannot be a render object widget', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
ViewAnchor(
|
|
view: const ColoredBox(color: Colors.red),
|
|
child: Container(),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith(
|
|
'The render object for ColoredBox cannot find ancestor render object to attach to.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'A RenderObject cannot be moved into the view property of a ViewAnchor via GlobalKey',
|
|
(WidgetTester tester) async {
|
|
final Widget globalKeyedWidget = ColoredBox(key: GlobalKey(), color: Colors.red);
|
|
|
|
await tester.pumpWidget(ViewAnchor(child: globalKeyedWidget));
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(ViewAnchor(view: globalKeyedWidget, child: const SizedBox()));
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot find ancestor render object to attach to.'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('ViewAnchor cannot be used at the top of the widget tree (outside of View)', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(wrapWithView: false, const ViewAnchor(child: SizedBox()));
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith(
|
|
'The render object for SizedBox cannot find ancestor render object to attach to.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets(
|
|
'ViewAnchor cannot be moved to the top of the widget tree (outside of View) via GlobalKey',
|
|
(WidgetTester tester) async {
|
|
final Widget globalKeyedViewAnchor = ViewAnchor(key: GlobalKey(), child: const SizedBox());
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(view: tester.view, child: globalKeyedViewAnchor),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, globalKeyedViewAnchor);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
contains('cannot find ancestor render object to attach to.'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('View can be used at the top of the widget tree', (WidgetTester tester) async {
|
|
await tester.pumpWidget(wrapWithView: false, View(view: tester.view, child: Container()));
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('View can be moved to the top of the widget tree view GlobalKey', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Widget globalKeyView = View(
|
|
view: FakeView(tester.view),
|
|
child: const ColoredBox(color: Colors.red),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
View(
|
|
view: tester.view,
|
|
child: ViewAnchor(
|
|
view: globalKeyView, // This one has trouble when deactivating
|
|
child: const SizedBox(),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(SizedBox), findsOneWidget);
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
|
|
await tester.pumpWidget(wrapWithView: false, globalKeyView);
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(SizedBox), findsNothing);
|
|
expect(find.byType(ColoredBox), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('ViewCollection can be used at the top of the widget tree', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[View(view: tester.view, child: Container())],
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('ViewCollection cannot be used inside a View', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
ViewCollection(
|
|
views: <Widget>[View(view: FakeView(tester.view), child: Container())],
|
|
),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith(
|
|
'The Element for ViewCollection cannot be inserted into slot "null" of its ancestor.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('ViewCollection can be used as ViewAnchor.view', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
ViewAnchor(
|
|
view: ViewCollection(
|
|
views: <Widget>[View(view: FakeView(tester.view), child: Container())],
|
|
),
|
|
child: Container(),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets('ViewCollection cannot have render object widgets as children', (
|
|
WidgetTester tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
const ViewCollection(views: <Widget>[ColoredBox(color: Colors.red)]),
|
|
);
|
|
|
|
expect(
|
|
tester.takeException(),
|
|
isFlutterError.having(
|
|
(FlutterError error) => error.message,
|
|
'message',
|
|
startsWith(
|
|
'The render object for ColoredBox cannot find ancestor render object to attach to.',
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Views can be moved in and out of ViewCollections via GlobalKey', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Widget greenView = View(
|
|
key: GlobalKey(debugLabel: 'green'),
|
|
view: tester.view,
|
|
child: const ColoredBox(color: Colors.green),
|
|
);
|
|
final Widget redView = View(
|
|
key: GlobalKey(debugLabel: 'red'),
|
|
view: FakeView(tester.view),
|
|
child: const ColoredBox(color: Colors.red),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
greenView,
|
|
ViewCollection(views: <Widget>[redView]),
|
|
],
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(ColoredBox), findsNWidgets(2));
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
redView,
|
|
ViewCollection(views: <Widget>[greenView]),
|
|
],
|
|
),
|
|
);
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(ColoredBox), findsNWidgets(2));
|
|
});
|
|
|
|
testWidgets('Can move stuff between views via global key: viewA -> viewB', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView greenView = tester.view;
|
|
final FlutterView redView = FakeView(tester.view);
|
|
final Widget globalKeyChild = SizedBox(key: GlobalKey());
|
|
|
|
Map<int, RenderObject> collectLeafRenderObjects() {
|
|
final Map<int, RenderObject> result = <int, RenderObject>{};
|
|
for (final RenderView renderView in RendererBinding.instance.renderViews) {
|
|
void visit(RenderObject object) {
|
|
result[renderView.flutterView.viewId] = object;
|
|
object.visitChildren(visit);
|
|
}
|
|
|
|
visit(renderView);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: greenView,
|
|
child: ColoredBox(color: Colors.green, child: globalKeyChild),
|
|
),
|
|
View(
|
|
view: redView,
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
|
|
|
Map<int, RenderObject> leafRenderObject = collectLeafRenderObjects();
|
|
expect(leafRenderObject[greenView.viewId], isA<RenderConstrainedBox>());
|
|
expect(leafRenderObject[redView.viewId], isNot(isA<RenderConstrainedBox>()));
|
|
|
|
// Move the child.
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: greenView,
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
View(
|
|
view: redView,
|
|
child: ColoredBox(color: Colors.red, child: globalKeyChild),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey));
|
|
|
|
leafRenderObject = collectLeafRenderObjects();
|
|
expect(leafRenderObject[greenView.viewId], isNot(isA<RenderConstrainedBox>()));
|
|
expect(leafRenderObject[redView.viewId], isA<RenderConstrainedBox>());
|
|
});
|
|
|
|
testWidgets('Can move stuff between views via global key: viewB -> viewA', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView greenView = tester.view;
|
|
final FlutterView redView = FakeView(tester.view);
|
|
final Widget globalKeyChild = SizedBox(key: GlobalKey());
|
|
|
|
Map<int, RenderObject> collectLeafRenderObjects() {
|
|
final Map<int, RenderObject> result = <int, RenderObject>{};
|
|
for (final RenderView renderView in RendererBinding.instance.renderViews) {
|
|
void visit(RenderObject object) {
|
|
result[renderView.flutterView.viewId] = object;
|
|
object.visitChildren(visit);
|
|
}
|
|
|
|
visit(renderView);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: greenView,
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
View(
|
|
view: redView,
|
|
child: ColoredBox(color: Colors.red, child: globalKeyChild),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
|
|
|
Map<int, RenderObject> leafRenderObject = collectLeafRenderObjects();
|
|
expect(leafRenderObject[redView.viewId], isA<RenderConstrainedBox>());
|
|
expect(leafRenderObject[greenView.viewId], isNot(isA<RenderConstrainedBox>()));
|
|
|
|
// Move the child.
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: greenView,
|
|
child: ColoredBox(color: Colors.green, child: globalKeyChild),
|
|
),
|
|
View(
|
|
view: redView,
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey));
|
|
|
|
leafRenderObject = collectLeafRenderObjects();
|
|
expect(leafRenderObject[redView.viewId], isNot(isA<RenderConstrainedBox>()));
|
|
expect(leafRenderObject[greenView.viewId], isA<RenderConstrainedBox>());
|
|
});
|
|
|
|
testWidgets('Can move stuff out of a view that is going away, viewA -> ViewB', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView greenView = tester.view;
|
|
final Key greenKey = UniqueKey();
|
|
final FlutterView redView = FakeView(tester.view);
|
|
final Key redKey = UniqueKey();
|
|
final Widget globalKeyChild = SizedBox(key: GlobalKey());
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
key: greenKey,
|
|
view: greenView,
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
View(
|
|
key: redKey,
|
|
view: redView,
|
|
child: ColoredBox(color: Colors.red, child: globalKeyChild),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
|
|
|
// Move the child and remove its view.
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
key: greenKey,
|
|
view: greenView,
|
|
child: ColoredBox(color: Colors.green, child: globalKeyChild),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(findsColoredBox(Colors.red), findsNothing);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey));
|
|
});
|
|
|
|
testWidgets('Can move stuff out of a view that is going away, viewB -> ViewA', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView greenView = tester.view;
|
|
final Key greenKey = UniqueKey();
|
|
final FlutterView redView = FakeView(tester.view);
|
|
final Key redKey = UniqueKey();
|
|
final Widget globalKeyChild = SizedBox(key: GlobalKey());
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
key: greenKey,
|
|
view: greenView,
|
|
child: ColoredBox(color: Colors.green, child: globalKeyChild),
|
|
),
|
|
View(
|
|
key: redKey,
|
|
view: redView,
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.green), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
|
|
|
// Move the child and remove its view.
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
key: redKey,
|
|
view: redView,
|
|
child: ColoredBox(color: Colors.red, child: globalKeyChild),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(findsColoredBox(Colors.green), findsNothing);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey));
|
|
});
|
|
|
|
testWidgets('Can move stuff out of a view that is moving itself, stuff ends up before view', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key key1 = UniqueKey();
|
|
final Key key2 = UniqueKey();
|
|
final Key key3 = UniqueKey();
|
|
final Key key4 = UniqueKey();
|
|
|
|
final GlobalKey viewKey = GlobalKey();
|
|
final GlobalKey childKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(key: key1),
|
|
ViewAnchor(
|
|
key: key2,
|
|
view: View(
|
|
key: viewKey,
|
|
view: FakeView(tester.view),
|
|
child: SizedBox(
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
),
|
|
child: const SizedBox(),
|
|
),
|
|
ViewAnchor(key: key3, child: const SizedBox()),
|
|
SizedBox(key: key4),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(
|
|
key: key1,
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
ViewAnchor(key: key2, child: const SizedBox()),
|
|
ViewAnchor(
|
|
key: key3,
|
|
view: View(key: viewKey, view: FakeView(tester.view), child: const SizedBox()),
|
|
child: const SizedBox(),
|
|
),
|
|
SizedBox(key: key4),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(key: key1),
|
|
ViewAnchor(
|
|
key: key2,
|
|
view: View(
|
|
key: viewKey,
|
|
view: FakeView(tester.view),
|
|
child: SizedBox(
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
),
|
|
child: const SizedBox(),
|
|
),
|
|
ViewAnchor(key: key3, child: const SizedBox()),
|
|
SizedBox(key: key4),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Can move stuff out of a view that is moving itself, stuff ends up after view', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final Key key1 = UniqueKey();
|
|
final Key key2 = UniqueKey();
|
|
final Key key3 = UniqueKey();
|
|
final Key key4 = UniqueKey();
|
|
|
|
final GlobalKey viewKey = GlobalKey();
|
|
final GlobalKey childKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(key: key1),
|
|
ViewAnchor(
|
|
key: key2,
|
|
view: View(
|
|
key: viewKey,
|
|
view: FakeView(tester.view),
|
|
child: SizedBox(
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
),
|
|
child: const SizedBox(),
|
|
),
|
|
ViewAnchor(key: key3, child: const SizedBox()),
|
|
SizedBox(key: key4),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(key: key1),
|
|
ViewAnchor(key: key2, child: const SizedBox()),
|
|
ViewAnchor(
|
|
key: key3,
|
|
view: View(key: viewKey, view: FakeView(tester.view), child: const SizedBox()),
|
|
child: const SizedBox(),
|
|
),
|
|
SizedBox(
|
|
key: key4,
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
SizedBox(key: key1),
|
|
ViewAnchor(
|
|
key: key2,
|
|
view: View(
|
|
key: viewKey,
|
|
view: FakeView(tester.view),
|
|
child: SizedBox(
|
|
child: ColoredBox(key: childKey, color: Colors.green),
|
|
),
|
|
),
|
|
child: const SizedBox(),
|
|
),
|
|
ViewAnchor(key: key3, child: const SizedBox()),
|
|
SizedBox(key: key4),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Can globalkey move down the tree from a view that is going away', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView anchorView = FakeView(tester.view);
|
|
final Widget globalKeyChild = SizedBox(key: GlobalKey());
|
|
|
|
await tester.pumpWidget(
|
|
ColoredBox(
|
|
color: Colors.green,
|
|
child: ViewAnchor(
|
|
view: View(
|
|
view: anchorView,
|
|
child: ColoredBox(color: Colors.yellow, child: globalKeyChild),
|
|
),
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(findsColoredBox(Colors.green), findsOneWidget);
|
|
expect(findsColoredBox(Colors.yellow), findsOneWidget);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.yellow), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(findsColoredBox(Colors.red), findsOneWidget);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
expect(find.byType(SizedBox), findsOneWidget);
|
|
final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!));
|
|
|
|
await tester.pumpWidget(
|
|
ColoredBox(
|
|
color: Colors.green,
|
|
child: ViewAnchor(
|
|
child: ColoredBox(color: Colors.red, child: globalKeyChild),
|
|
),
|
|
),
|
|
);
|
|
expect(findsColoredBox(Colors.green), findsOneWidget);
|
|
expect(findsColoredBox(Colors.yellow), findsNothing);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.yellow), matching: find.byType(SizedBox)),
|
|
findsNothing,
|
|
);
|
|
expect(findsColoredBox(Colors.red), findsOneWidget);
|
|
expect(
|
|
find.descendant(of: findsColoredBox(Colors.red), matching: find.byType(SizedBox)),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.byType(SizedBox), findsOneWidget);
|
|
expect(tester.renderObject(find.byKey(globalKeyChild.key!)), boxWithGlobalKey);
|
|
});
|
|
|
|
testWidgets('RenderObjects are disposed when a view goes away from a ViewAnchor', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView anchorView = FakeView(tester.view);
|
|
|
|
await tester.pumpWidget(
|
|
ColoredBox(
|
|
color: Colors.green,
|
|
child: ViewAnchor(
|
|
view: View(
|
|
view: anchorView,
|
|
child: const ColoredBox(color: Colors.yellow),
|
|
),
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderObject box = tester.renderObject(findsColoredBox(Colors.yellow));
|
|
|
|
await tester.pumpWidget(
|
|
const ColoredBox(
|
|
color: Colors.green,
|
|
child: ViewAnchor(child: ColoredBox(color: Colors.red)),
|
|
),
|
|
);
|
|
|
|
expect(box.debugDisposed, isTrue);
|
|
});
|
|
|
|
testWidgets('RenderObjects are disposed when a view goes away from a ViewCollection', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final FlutterView redView = tester.view;
|
|
final FlutterView greenView = FakeView(tester.view);
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: redView,
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
View(
|
|
view: greenView,
|
|
child: const ColoredBox(color: Colors.green),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(findsColoredBox(Colors.green), findsOneWidget);
|
|
expect(findsColoredBox(Colors.red), findsOneWidget);
|
|
final RenderObject box = tester.renderObject(findsColoredBox(Colors.green));
|
|
|
|
await tester.pumpWidget(
|
|
wrapWithView: false,
|
|
ViewCollection(
|
|
views: <Widget>[
|
|
View(
|
|
view: redView,
|
|
child: const ColoredBox(color: Colors.red),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(findsColoredBox(Colors.green), findsNothing);
|
|
expect(findsColoredBox(Colors.red), findsOneWidget);
|
|
expect(box.debugDisposed, isTrue);
|
|
});
|
|
|
|
testWidgets('View can be wrapped and unwrapped', (WidgetTester tester) async {
|
|
final Widget view = View(view: tester.view, child: const SizedBox());
|
|
|
|
await tester.pumpWidget(wrapWithView: false, view);
|
|
|
|
final RenderObject renderView = tester.renderObject(find.byType(View));
|
|
final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox));
|
|
|
|
await tester.pumpWidget(wrapWithView: false, ViewCollection(views: <Widget>[view]));
|
|
|
|
expect(tester.renderObject(find.byType(View)), same(renderView));
|
|
expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox));
|
|
|
|
await tester.pumpWidget(wrapWithView: false, view);
|
|
|
|
expect(tester.renderObject(find.byType(View)), same(renderView));
|
|
expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox));
|
|
});
|
|
|
|
testWidgets('ViewAnchor with View can be wrapped and unwrapped', (WidgetTester tester) async {
|
|
final Widget viewAnchor = ViewAnchor(
|
|
view: View(view: FakeView(tester.view), child: const SizedBox()),
|
|
child: const ColoredBox(color: Colors.green),
|
|
);
|
|
|
|
await tester.pumpWidget(viewAnchor);
|
|
|
|
final List<RenderObject> renderViews = tester.renderObjectList(find.byType(View)).toList();
|
|
final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox));
|
|
|
|
await tester.pumpWidget(ColoredBox(color: Colors.yellow, child: viewAnchor));
|
|
|
|
expect(tester.renderObjectList(find.byType(View)), renderViews);
|
|
expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox));
|
|
|
|
await tester.pumpWidget(viewAnchor);
|
|
|
|
expect(tester.renderObjectList(find.byType(View)), renderViews);
|
|
expect(tester.renderObject(find.byType(SizedBox)), same(renderSizedBox));
|
|
});
|
|
|
|
testWidgets('Moving a View keeps its semantics tree stable', (WidgetTester tester) async {
|
|
final Widget view = View(
|
|
// No explicit key, we rely on the implicit key of the underlying RawView.
|
|
view: tester.view,
|
|
child: Semantics(textDirection: TextDirection.ltr, label: 'Hello', child: const SizedBox()),
|
|
);
|
|
await tester.pumpWidget(wrapWithView: false, view);
|
|
|
|
final RenderObject renderSemantics = tester.renderObject(find.bySemanticsLabel('Hello'));
|
|
final SemanticsNode semantics = tester.getSemantics(find.bySemanticsLabel('Hello'));
|
|
expect(semantics.id, 1);
|
|
expect(renderSemantics.debugSemantics, same(semantics));
|
|
|
|
await tester.pumpWidget(wrapWithView: false, ViewCollection(views: <Widget>[view]));
|
|
|
|
final RenderObject renderSemanticsAfterMove = tester.renderObject(
|
|
find.bySemanticsLabel('Hello'),
|
|
);
|
|
final SemanticsNode semanticsAfterMove = tester.getSemantics(find.bySemanticsLabel('Hello'));
|
|
expect(renderSemanticsAfterMove, same(renderSemantics));
|
|
expect(semanticsAfterMove.id, 1);
|
|
expect(semanticsAfterMove, same(semantics));
|
|
});
|
|
}
|
|
|
|
Finder findsColoredBox(Color color) {
|
|
return find.byWidgetPredicate((Widget widget) => widget is ColoredBox && widget.color == color);
|
|
}
|