// 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/rendering.dart'; import 'package:flutter/widgets.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() { const red = Color(0xffffff00); const green = Color(0xff00ff00); const yellow = Color(0xfffffde7); 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: 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: 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: 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: 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: 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: 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: [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: [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: [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: [ColoredBox(color: 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: green), ); final Widget redView = View( key: GlobalKey(debugLabel: 'red'), view: FakeView(tester.view), child: const ColoredBox(color: red), ); await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ greenView, ViewCollection(views: [redView]), ], ), ); expect(tester.takeException(), isNull); expect(find.byType(ColoredBox), findsNWidgets(2)); await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ redView, ViewCollection(views: [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 collectLeafRenderObjects() { final result = {}; 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: [ View( view: greenView, child: ColoredBox(color: green, child: globalKeyChild), ), View( view: redView, child: const ColoredBox(color: red), ), ], ), ); expect( find.descendant(of: findsColoredBox(green), matching: find.byType(SizedBox)), findsOneWidget, ); expect( find.descendant(of: findsColoredBox(red), matching: find.byType(SizedBox)), findsNothing, ); final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); Map leafRenderObject = collectLeafRenderObjects(); expect(leafRenderObject[greenView.viewId], isA()); expect(leafRenderObject[redView.viewId], isNot(isA())); // Move the child. await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ View( view: greenView, child: const ColoredBox(color: green), ), View( view: redView, child: ColoredBox(color: red, child: globalKeyChild), ), ], ), ); expect( find.descendant(of: findsColoredBox(green), matching: find.byType(SizedBox)), findsNothing, ); expect( find.descendant(of: findsColoredBox(red), matching: find.byType(SizedBox)), findsOneWidget, ); expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey)); leafRenderObject = collectLeafRenderObjects(); expect(leafRenderObject[greenView.viewId], isNot(isA())); expect(leafRenderObject[redView.viewId], isA()); }); 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 collectLeafRenderObjects() { final result = {}; 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: [ View( view: greenView, child: const ColoredBox(color: green), ), View( view: redView, child: ColoredBox(color: red, child: globalKeyChild), ), ], ), ); expect( find.descendant(of: findsColoredBox(red), matching: find.byType(SizedBox)), findsOneWidget, ); expect( find.descendant(of: findsColoredBox(green), matching: find.byType(SizedBox)), findsNothing, ); final RenderObject boxWithGlobalKey = tester.renderObject(find.byKey(globalKeyChild.key!)); Map leafRenderObject = collectLeafRenderObjects(); expect(leafRenderObject[redView.viewId], isA()); expect(leafRenderObject[greenView.viewId], isNot(isA())); // Move the child. await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ View( view: greenView, child: ColoredBox(color: green, child: globalKeyChild), ), View( view: redView, child: const ColoredBox(color: red), ), ], ), ); expect( find.descendant(of: findsColoredBox(red), matching: find.byType(SizedBox)), findsNothing, ); expect( find.descendant(of: findsColoredBox(green), matching: find.byType(SizedBox)), findsOneWidget, ); expect(tester.renderObject(find.byKey(globalKeyChild.key!)), equals(boxWithGlobalKey)); leafRenderObject = collectLeafRenderObjects(); expect(leafRenderObject[redView.viewId], isNot(isA())); expect(leafRenderObject[greenView.viewId], isA()); }); 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: [ View( key: greenKey, view: greenView, child: const ColoredBox(color: green), ), View( key: redKey, view: redView, child: ColoredBox(color: red, child: globalKeyChild), ), ], ), ); expect( find.descendant(of: findsColoredBox(red), matching: find.byType(SizedBox)), findsOneWidget, ); expect( find.descendant(of: findsColoredBox(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: [ View( key: greenKey, view: greenView, child: ColoredBox(color: green, child: globalKeyChild), ), ], ), ); expect(findsColoredBox(red), findsNothing); expect( find.descendant(of: findsColoredBox(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: [ View( key: greenKey, view: greenView, child: ColoredBox(color: green, child: globalKeyChild), ), View( key: redKey, view: redView, child: const ColoredBox(color: red), ), ], ), ); expect( find.descendant(of: findsColoredBox(green), matching: find.byType(SizedBox)), findsOneWidget, ); expect( find.descendant(of: findsColoredBox(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: [ View( key: redKey, view: redView, child: ColoredBox(color: red, child: globalKeyChild), ), ], ), ); expect(findsColoredBox(green), findsNothing); expect( find.descendant(of: findsColoredBox(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: [ SizedBox(key: key1), ViewAnchor( key: key2, view: View( key: viewKey, view: FakeView(tester.view), child: SizedBox( child: ColoredBox(key: childKey, color: green), ), ), child: const SizedBox(), ), ViewAnchor(key: key3, child: const SizedBox()), SizedBox(key: key4), ], ), ); await tester.pumpWidget( Column( children: [ SizedBox( key: key1, child: ColoredBox(key: childKey, color: 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: [ SizedBox(key: key1), ViewAnchor( key: key2, view: View( key: viewKey, view: FakeView(tester.view), child: SizedBox( child: ColoredBox(key: childKey, color: 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: [ SizedBox(key: key1), ViewAnchor( key: key2, view: View( key: viewKey, view: FakeView(tester.view), child: SizedBox( child: ColoredBox(key: childKey, color: green), ), ), child: const SizedBox(), ), ViewAnchor(key: key3, child: const SizedBox()), SizedBox(key: key4), ], ), ); await tester.pumpWidget( Column( children: [ 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: green), ), ], ), ); await tester.pumpWidget( Column( children: [ SizedBox(key: key1), ViewAnchor( key: key2, view: View( key: viewKey, view: FakeView(tester.view), child: SizedBox( child: ColoredBox(key: childKey, color: 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: green, child: ViewAnchor( view: View( view: anchorView, child: ColoredBox(color: yellow, child: globalKeyChild), ), child: const ColoredBox(color: red), ), ), ); expect(findsColoredBox(green), findsOneWidget); expect(findsColoredBox(yellow), findsOneWidget); expect( find.descendant(of: findsColoredBox(yellow), matching: find.byType(SizedBox)), findsOneWidget, ); expect(findsColoredBox(red), findsOneWidget); expect( find.descendant(of: findsColoredBox(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: green, child: ViewAnchor( child: ColoredBox(color: red, child: globalKeyChild), ), ), ); expect(findsColoredBox(green), findsOneWidget); expect(findsColoredBox(yellow), findsNothing); expect( find.descendant(of: findsColoredBox(yellow), matching: find.byType(SizedBox)), findsNothing, ); expect(findsColoredBox(red), findsOneWidget); expect( find.descendant(of: findsColoredBox(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: green, child: ViewAnchor( view: View( view: anchorView, child: const ColoredBox(color: yellow), ), child: const ColoredBox(color: red), ), ), ); final RenderObject box = tester.renderObject(findsColoredBox(yellow)); await tester.pumpWidget( const ColoredBox( color: green, child: ViewAnchor(child: ColoredBox(color: 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: [ View( view: redView, child: const ColoredBox(color: red), ), View( view: greenView, child: const ColoredBox(color: green), ), ], ), ); expect(findsColoredBox(green), findsOneWidget); expect(findsColoredBox(red), findsOneWidget); final RenderObject box = tester.renderObject(findsColoredBox(green)); await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ View( view: redView, child: const ColoredBox(color: red), ), ], ), ); expect(findsColoredBox(green), findsNothing); expect(findsColoredBox(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: [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: green), ); await tester.pumpWidget(viewAnchor); final List renderViews = tester.renderObjectList(find.byType(View)).toList(); final RenderObject renderSizedBox = tester.renderObject(find.byType(SizedBox)); await tester.pumpWidget(ColoredBox(color: 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: [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); }