blob: 495621fe907b2bd8cd31f35ca4974e0dfa755aee [file] [log] [blame] [edit]
// 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<FlutterView>());
expect(viewMaybeOf, isNotNull);
expect(viewMaybeOf, isA<FlutterView>());
});
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<FlutterView>());
expect(viewMaybeOf, isNotNull);
expect(viewMaybeOf, isA<FlutterView>());
});
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<FlutterError>().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 = <PipelineOwner>[];
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: <Widget>[
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: <Widget>[]), 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 = <String>[];
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, <String>['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 = <Element>[];
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: <Widget>[
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 = <Element>[];
viewAnchorElement.visitChildren((Element element) {
children.add(element);
});
expect(children, hasLength(3));
await tester.pumpWidget(
wrapWithView: false,
ViewCollection(
views: <Widget>[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<RenderView>());
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: <Widget>[
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<RenderConstrainedBox>());
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<RenderView>(find.byKey(viewKey));
expect(RendererBinding.instance.renderViews, contains(rawView));
final children = <PipelineOwner>[];
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<RenderView>(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<RenderView>(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<RenderView>(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<RenderView>(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: <Widget>[
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<ViewFocusEvent> 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: <Widget>[
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<ViewFocusEvent> 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<String> 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<String> log;
@override
void performLayout() {
log.add('layout $label');
super.performLayout();
}
}