blob: 64221196bb6f0b26e0b8a144cecae6a7a3f20103 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '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('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 List<PipelineOwner> 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 List<String> 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: Colors.green),
),
child: const SizedBox(),
),
);
final Element viewAnchorElement = tester.element(find.byElementPredicate((Element e) => e.runtimeType.toString() == '_MultiChildComponentElement'));
final List<Element> 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 List<Element> 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: Colors.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: Colors.green),
),
);
expect(parentPipelineOwner, isNot(RendererBinding.instance.rootPipelineOwner));
final RenderView rawView = tester.renderObject<RenderView>(find.byKey(viewKey));
expect(RendererBinding.instance.renderViews, contains(rawView));
final List<PipelineOwner> 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: Colors.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 Size 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 = 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 = Size(30, 60);
const ViewConstraints viewConstraints = ViewConstraints(maxWidth: 333, maxHeight: 666);
final BoxConstraints 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 = Size(3000, 6000);
const ViewConstraints 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));
});
}
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();
}
}