blob: b0a87c09ffb8e2b3862e961a58672b3c5be369d4 [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';
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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 must have one view', (WidgetTester tester) async {
expect(() => ViewCollection(views: const <Widget>[]), throwsAssertionError);
});
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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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', (WidgetTester tester) async {
await pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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 pumpWidgetWithoutViewWrapper(
tester: tester,
widget: 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)));
});
}
Future<void> pumpWidgetWithoutViewWrapper({required WidgetTester tester, required Widget widget}) {
tester.binding.attachRootWidget(widget);
tester.binding.scheduleFrame();
return tester.binding.pump();
}
class FakeView extends TestFlutterView{
FakeView(FlutterView view, { this.viewId = 100 }) : super(
view: view,
platformDispatcher: view.platformDispatcher as TestPlatformDispatcher,
display: view.display as TestDisplay,
);
@override
final int viewId;
}
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();
}
}