// 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/services.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 green = Color(0xff00ff00); testWidgets('Widgets running with runApp can find View', (WidgetTester tester) async { FlutterView? viewOf; FlutterView? viewMaybeOf; runApp( Builder( builder: (BuildContext context) { viewOf = View.of(context); viewMaybeOf = View.maybeOf(context); return Container(); }, ), ); expect(viewOf, isNotNull); expect(viewOf, isA()); expect(viewMaybeOf, isNotNull); expect(viewMaybeOf, isA()); }); testWidgets('Widgets running with pumpWidget can find View', (WidgetTester tester) async { FlutterView? view; FlutterView? viewMaybeOf; await tester.pumpWidget( Builder( builder: (BuildContext context) { view = View.of(context); viewMaybeOf = View.maybeOf(context); return Container(); }, ), ); expect(view, isNotNull); expect(view, isA()); expect(viewMaybeOf, isNotNull); expect(viewMaybeOf, isA()); }); testWidgets('cannot find View behind a LookupBoundary', (WidgetTester tester) async { await tester.pumpWidget(LookupBoundary(child: Container())); final BuildContext context = tester.element(find.byType(Container)); expect(View.maybeOf(context), isNull); expect( () => View.of(context), throwsA( isA().having( (FlutterError error) => error.message, 'message', contains( 'The context provided to View.of() does have a View widget ancestor, but it is hidden by a LookupBoundary.', ), ), ), ); }); testWidgets('child of view finds view, parentPipelineOwner, mediaQuery', ( WidgetTester tester, ) async { FlutterView? outsideView; FlutterView? insideView; PipelineOwner? outsideParent; PipelineOwner? insideParent; await tester.pumpWidget( wrapWithView: false, Builder( builder: (BuildContext context) { outsideView = View.maybeOf(context); outsideParent = View.pipelineOwnerOf(context); return View( view: tester.view, child: Builder( builder: (BuildContext context) { insideView = View.maybeOf(context); insideParent = View.pipelineOwnerOf(context); return const SizedBox(); }, ), ); }, ), ); expect(outsideView, isNull); expect(insideView, equals(tester.view)); expect(outsideParent, isNotNull); expect(insideParent, isNotNull); expect(outsideParent, isNot(equals(insideParent))); expect(outsideParent, tester.binding.rootPipelineOwner); expect(insideParent, equals(tester.renderObject(find.byType(SizedBox)).owner)); final pipelineOwners = []; tester.binding.rootPipelineOwner.visitChildren((PipelineOwner child) { pipelineOwners.add(child); }); expect(pipelineOwners.single, equals(insideParent)); }); testWidgets('cannot have multiple views with same FlutterView', (WidgetTester tester) async { await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ View(view: tester.view, child: const SizedBox()), View(view: tester.view, child: const SizedBox()), ], ), ); expect( tester.takeException(), isFlutterError.having( (FlutterError e) => e.message, 'message', contains('Multiple widgets used the same GlobalKey'), ), ); }); testWidgets('ViewCollection may start with zero views', (WidgetTester tester) async { expect(() => const ViewCollection(views: []), returnsNormally); }); testWidgets('ViewAnchor.child does not see surrounding view', (WidgetTester tester) async { FlutterView? inside; FlutterView? outside; await tester.pumpWidget( Builder( builder: (BuildContext context) { outside = View.maybeOf(context); return ViewAnchor( view: Builder( builder: (BuildContext context) { inside = View.maybeOf(context); return View(view: FakeView(tester.view), child: const SizedBox()); }, ), child: const SizedBox(), ); }, ), ); expect(inside, isNull); expect(outside, isNotNull); }); testWidgets('ViewAnchor layout order', (WidgetTester tester) async { Finder findSpyWidget(int label) { return find.byWidgetPredicate((Widget w) => w is SpyRenderWidget && w.label == label); } final log = []; await tester.pumpWidget( SpyRenderWidget( label: 1, log: log, child: ViewAnchor( view: View( view: FakeView(tester.view), child: SpyRenderWidget(label: 2, log: log), ), child: SpyRenderWidget(label: 3, log: log), ), ), ); log.clear(); tester.renderObject(findSpyWidget(3)).markNeedsLayout(); tester.renderObject(findSpyWidget(2)).markNeedsLayout(); tester.renderObject(findSpyWidget(1)).markNeedsLayout(); await tester.pump(); expect(log, ['layout 1', 'layout 3', 'layout 2']); }); testWidgets('visitChildren of ViewAnchor visits both children', (WidgetTester tester) async { await tester.pumpWidget( ViewAnchor( view: View( view: FakeView(tester.view), child: const ColoredBox(color: green), ), child: const SizedBox(), ), ); final Element viewAnchorElement = tester.element( find.byElementPredicate( (Element e) => e.runtimeType.toString() == '_MultiChildComponentElement', ), ); final children = []; viewAnchorElement.visitChildren((Element element) { children.add(element); }); expect(children, hasLength(2)); await tester.pumpWidget(const ViewAnchor(child: SizedBox())); children.clear(); viewAnchorElement.visitChildren((Element element) { children.add(element); }); expect(children, hasLength(1)); }); testWidgets('visitChildren of ViewCollection visits all children', (WidgetTester tester) async { await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [ View(view: tester.view, child: const SizedBox()), View(view: FakeView(tester.view), child: const SizedBox()), View(view: FakeView(tester.view, viewId: 423), child: const SizedBox()), ], ), ); final Element viewAnchorElement = tester.element( find.byElementPredicate( (Element e) => e.runtimeType.toString() == '_MultiChildComponentElement', ), ); final children = []; viewAnchorElement.visitChildren((Element element) { children.add(element); }); expect(children, hasLength(3)); await tester.pumpWidget( wrapWithView: false, ViewCollection( views: [View(view: tester.view, child: const SizedBox())], ), ); children.clear(); viewAnchorElement.visitChildren((Element element) { children.add(element); }); expect(children, hasLength(1)); }); group('renderObject getter', () { testWidgets('ancestors of view see RenderView as renderObject', (WidgetTester tester) async { late BuildContext builderContext; await tester.pumpWidget( wrapWithView: false, Builder( builder: (BuildContext context) { builderContext = context; return View(view: tester.view, child: const SizedBox()); }, ), ); final RenderObject? renderObject = builderContext.findRenderObject(); expect(renderObject, isNotNull); expect(renderObject, isA()); expect(renderObject, tester.renderObject(find.byType(View))); expect(tester.element(find.byType(Builder)).renderObject, renderObject); }); testWidgets('ancestors of ViewCollection get null for renderObject', ( WidgetTester tester, ) async { late BuildContext builderContext; await tester.pumpWidget( wrapWithView: false, Builder( builder: (BuildContext context) { builderContext = context; return ViewCollection( views: [ View(view: tester.view, child: const SizedBox()), View(view: FakeView(tester.view), child: const SizedBox()), ], ); }, ), ); final RenderObject? renderObject = builderContext.findRenderObject(); expect(renderObject, isNull); expect(tester.element(find.byType(Builder)).renderObject, isNull); }); testWidgets('ancestors of a ViewAnchor see the right RenderObject', ( WidgetTester tester, ) async { late BuildContext builderContext; await tester.pumpWidget( Builder( builder: (BuildContext context) { builderContext = context; return ViewAnchor( view: View( view: FakeView(tester.view), child: const ColoredBox(color: green), ), child: const SizedBox(), ); }, ), ); final RenderObject? renderObject = builderContext.findRenderObject(); expect(renderObject, isNotNull); expect(renderObject, isA()); expect(renderObject, tester.renderObject(find.byType(SizedBox))); expect(tester.element(find.byType(Builder)).renderObject, renderObject); }); }); testWidgets( 'correctly switches between view configurations', experimentalLeakTesting: LeakTesting.settings .withIgnoredAll(), // Leaking by design as contains deprecated items. (WidgetTester tester) async { await tester.pumpWidget( wrapWithView: false, View( view: tester.view, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, child: const SizedBox(), ), ); RenderObject renderView = tester.renderObject(find.byType(View)); expect(renderView, same(tester.binding.renderView)); expect(renderView.owner, same(tester.binding.pipelineOwner)); expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner)); await tester.pumpWidget( wrapWithView: false, View(view: tester.view, child: const SizedBox()), ); renderView = tester.renderObject(find.byType(View)); expect(renderView, isNot(same(tester.binding.renderView))); expect(renderView.owner, isNot(same(tester.binding.pipelineOwner))); expect( tester.renderObject(find.byType(SizedBox)).owner, isNot(same(tester.binding.pipelineOwner)), ); await tester.pumpWidget( wrapWithView: false, View( view: tester.view, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, child: const SizedBox(), ), ); renderView = tester.renderObject(find.byType(View)); expect(renderView, same(tester.binding.renderView)); expect(renderView.owner, same(tester.binding.pipelineOwner)); expect(tester.renderObject(find.byType(SizedBox)).owner, same(tester.binding.pipelineOwner)); expect( () => View( view: tester.view, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, child: const SizedBox(), ), throwsAssertionError, ); expect( () => View( view: tester.view, deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, child: const SizedBox(), ), throwsAssertionError, ); expect( () => View( view: FakeView(tester.view), deprecatedDoNotUseWillBeRemovedWithoutNoticeRenderView: tester.binding.renderView, deprecatedDoNotUseWillBeRemovedWithoutNoticePipelineOwner: tester.binding.pipelineOwner, child: const SizedBox(), ), throwsAssertionError, ); }, ); testWidgets('attaches itself correctly', (WidgetTester tester) async { final Key viewKey = UniqueKey(); late final PipelineOwner parentPipelineOwner; await tester.pumpWidget( ViewAnchor( view: Builder( builder: (BuildContext context) { parentPipelineOwner = View.pipelineOwnerOf(context); return View(key: viewKey, view: FakeView(tester.view), child: const SizedBox()); }, ), child: const ColoredBox(color: green), ), ); expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner)); final RenderView rawView = tester.renderObject(find.byKey(viewKey)); expect(RendererBinding.instance.renderViews, contains(rawView)); final children = []; parentPipelineOwner.visitChildren((PipelineOwner child) { children.add(child); }); final PipelineOwner rawViewOwner = rawView.owner!; expect(children, contains(rawViewOwner)); // Remove that View from the tree. await tester.pumpWidget(const ViewAnchor(child: ColoredBox(color: green))); expect(rawView.owner, isNull); expect(RendererBinding.instance.renderViews, isNot(contains(rawView))); children.clear(); parentPipelineOwner.visitChildren((PipelineOwner child) { children.add(child); }); expect(children, isNot(contains(rawViewOwner))); }); testWidgets('RenderView does not use size of child if constraints are tight', ( WidgetTester tester, ) async { const physicalSize = Size(300, 600); final Size logicalSize = physicalSize / tester.view.devicePixelRatio; tester.view.physicalConstraints = ViewConstraints.tight(physicalSize); await tester.pumpWidget(const Placeholder()); final RenderView renderView = tester.renderObject(find.byType(View)); expect(renderView.constraints, BoxConstraints.tight(logicalSize)); expect(renderView.size, logicalSize); final RenderBox child = renderView.child!; expect(child.constraints, BoxConstraints.tight(logicalSize)); expect(child.debugCanParentUseSize, isFalse); expect(child.size, logicalSize); }); testWidgets('RenderView sizes itself to child if constraints allow it (unconstrained)', ( WidgetTester tester, ) async { const size = Size(300, 600); tester.view.physicalConstraints = const ViewConstraints(); // unconstrained await tester.pumpWidget(SizedBox.fromSize(size: size)); final RenderView renderView = tester.renderObject(find.byType(View)); expect(renderView.constraints, const BoxConstraints()); expect(renderView.size, size); final RenderBox child = renderView.child!; expect(child.constraints, const BoxConstraints()); expect(child.debugCanParentUseSize, isTrue); expect(child.size, size); }); testWidgets('RenderView sizes itself to child if constraints allow it (constrained)', ( WidgetTester tester, ) async { const size = Size(30, 60); const viewConstraints = ViewConstraints(maxWidth: 333, maxHeight: 666); final boxConstraints = BoxConstraints.fromViewConstraints( viewConstraints / tester.view.devicePixelRatio, ); tester.view.physicalConstraints = viewConstraints; await tester.pumpWidget(SizedBox.fromSize(size: size)); final RenderView renderView = tester.renderObject(find.byType(View)); expect(renderView.constraints, boxConstraints); expect(renderView.size, size); final RenderBox child = renderView.child!; expect(child.constraints, boxConstraints); expect(child.debugCanParentUseSize, isTrue); expect(child.size, size); }); testWidgets('RenderView respects constraints when child wants to be bigger than allowed', ( WidgetTester tester, ) async { const size = Size(3000, 6000); const viewConstraints = ViewConstraints(maxWidth: 300, maxHeight: 600); tester.view.physicalConstraints = viewConstraints; await tester.pumpWidget(SizedBox.fromSize(size: size)); final RenderView renderView = tester.renderObject(find.byType(View)); expect(renderView.size, const Size(100, 200)); // viewConstraints.biggest / devicePixelRatio final RenderBox child = renderView.child!; expect(child.debugCanParentUseSize, isTrue); expect(child.size, const Size(100, 200)); }); testWidgets('ViewFocusEvents cause unfocusing and refocusing', (WidgetTester tester) async { late FlutterView view; late FocusNode focusNode; await tester.pumpWidget( Focus( child: Builder( builder: (BuildContext context) { view = View.of(context); focusNode = Focus.of(context); return Container(); }, ), ), ); final unfocusEvent = ViewFocusEvent( viewId: view.viewId, state: ViewFocusState.unfocused, direction: ViewFocusDirection.forward, ); final focusEvent = ViewFocusEvent( viewId: view.viewId, state: ViewFocusState.focused, direction: ViewFocusDirection.backward, ); focusNode.requestFocus(); await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse); ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(unfocusEvent); await tester.pump(); expect(focusNode.hasPrimaryFocus, isFalse); expect(FocusManager.instance.rootScope.hasPrimaryFocus, isTrue); ServicesBinding.instance.platformDispatcher.onViewFocusChange?.call(focusEvent); await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); expect(FocusManager.instance.rootScope.hasPrimaryFocus, isFalse); }); testWidgets( 'View notifies engine that a view should have focus when a widget focus change occurs.', (WidgetTester tester) async { final nodeA = FocusNode(debugLabel: 'a'); addTearDown(nodeA.dispose); FlutterView? view; await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: Column( children: [ Focus(focusNode: nodeA, child: const Text('a')), Builder( builder: (BuildContext context) { view = View.of(context); return const SizedBox.shrink(); }, ), ], ), ), ); var notifyCount = 0; void handleFocusChange() { notifyCount++; } tester.binding.focusManager.addListener(handleFocusChange); addTearDown(() => tester.binding.focusManager.removeListener(handleFocusChange)); tester.binding.platformDispatcher.resetFocusedViewTestValues(); nodeA.requestFocus(); await tester.pump(); final List events = tester.binding.platformDispatcher.testFocusEvents; expect(events.length, equals(1)); expect(events.last.viewId, equals(view?.viewId)); expect(events.last.direction, equals(ViewFocusDirection.forward)); expect(events.last.state, equals(ViewFocusState.focused)); expect(nodeA.hasPrimaryFocus, isTrue); expect(notifyCount, equals(1)); notifyCount = 0; }, ); testWidgets('Switching focus between views yields the correct events.', ( WidgetTester tester, ) async { final nodeA = FocusNode(debugLabel: 'a'); addTearDown(nodeA.dispose); FlutterView? view; await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: Column( children: [ Focus(focusNode: nodeA, child: const Text('a')), Builder( builder: (BuildContext context) { view = View.of(context); return const SizedBox.shrink(); }, ), ], ), ), ); var notifyCount = 0; void handleFocusChange() { notifyCount++; } tester.binding.focusManager.addListener(handleFocusChange); addTearDown(() => tester.binding.focusManager.removeListener(handleFocusChange)); tester.binding.platformDispatcher.resetFocusedViewTestValues(); // Focus and make sure engine is notified. nodeA.requestFocus(); await tester.pump(); List events = tester.binding.platformDispatcher.testFocusEvents; expect(events.length, equals(1)); expect(events.last.viewId, equals(view?.viewId)); expect(events.last.direction, equals(ViewFocusDirection.forward)); expect(events.last.state, equals(ViewFocusState.focused)); expect(nodeA.hasPrimaryFocus, isTrue); expect(notifyCount, equals(1)); notifyCount = 0; tester.binding.platformDispatcher.resetFocusedViewTestValues(); // Unfocus all views. tester.binding.platformDispatcher.onViewFocusChange?.call( ViewFocusEvent( viewId: view!.viewId, state: ViewFocusState.unfocused, direction: ViewFocusDirection.forward, ), ); await tester.pump(); expect(nodeA.hasFocus, isFalse); expect(tester.binding.platformDispatcher.testFocusEvents, isEmpty); expect(notifyCount, equals(1)); notifyCount = 0; tester.binding.platformDispatcher.resetFocusedViewTestValues(); // Focus another view. tester.binding.platformDispatcher.onViewFocusChange?.call( const ViewFocusEvent( viewId: 100, state: ViewFocusState.focused, direction: ViewFocusDirection.forward, ), ); // Focusing another view should unfocus this node without notifying the // engine to unfocus. await tester.pump(); expect(nodeA.hasFocus, isFalse); expect(tester.binding.platformDispatcher.testFocusEvents, isEmpty); expect(notifyCount, equals(0)); notifyCount = 0; tester.binding.platformDispatcher.resetFocusedViewTestValues(); // Re-focusing the node should notify the engine that this view is focused. nodeA.requestFocus(); await tester.pump(); expect(nodeA.hasPrimaryFocus, isTrue); events = tester.binding.platformDispatcher.testFocusEvents; expect(events.length, equals(1)); expect(events.last.viewId, equals(view?.viewId)); expect(events.last.direction, equals(ViewFocusDirection.forward)); expect(events.last.state, equals(ViewFocusState.focused)); expect(notifyCount, equals(1)); notifyCount = 0; tester.binding.platformDispatcher.resetFocusedViewTestValues(); }); } class SpyRenderWidget extends SizedBox { const SpyRenderWidget({super.key, required this.label, required this.log, super.child}); final int label; final List log; @override RenderSpy createRenderObject(BuildContext context) { return RenderSpy(additionalConstraints: const BoxConstraints(), label: label, log: log); } @override void updateRenderObject(BuildContext context, RenderSpy renderObject) { renderObject ..label = label ..log = log; } } class RenderSpy extends RenderConstrainedBox { RenderSpy({required super.additionalConstraints, required this.label, required this.log}); int label; List log; @override void performLayout() { log.add('layout $label'); super.performLayout(); } }