blob: fbbbe0f89764280e4e290bd028ce5dea56a0dedc [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.
// @dart = 2.8
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
@immutable
class Pair<T> {
const Pair(this.first, this.second);
final T first;
final T second;
@override
bool operator ==(Object other) {
return other is Pair<T> && other.first == first && other.second == second;
}
@override
int get hashCode => hashValues(first, second);
@override
String toString() => '($first,$second)';
}
/// Widget that will layout one child in the top half of this widget's size
/// and the other child in the bottom half. It will swap which child is on top
/// and which is on bottom every time the widget is rendered.
abstract class Swapper extends RenderObjectWidget {
const Swapper({this.stable, this.swapper});
final Widget stable;
final Widget swapper;
@override
SwapperElement createElement();
@override
RenderObject createRenderObject(BuildContext context) => RenderSwapper();
}
class SwapperWithProperOverrides extends Swapper {
const SwapperWithProperOverrides({
Widget stable,
Widget swapper,
}) : super(stable: stable, swapper: swapper);
@override
SwapperElement createElement() => SwapperElementWithProperOverrides(this);
}
class SwapperWithNoOverrides extends Swapper {
const SwapperWithNoOverrides({
Widget stable,
Widget swapper,
}) : super(stable: stable, swapper: swapper);
@override
SwapperElement createElement() => SwapperElementWithNoOverrides(this);
}
class SwapperWithDeprecatedOverrides extends Swapper {
const SwapperWithDeprecatedOverrides({
Widget stable,
Widget swapper,
}) : super(stable: stable, swapper: swapper);
@override
SwapperElement createElement() => SwapperElementWithDeprecatedOverrides(this);
}
abstract class SwapperElement extends RenderObjectElement {
SwapperElement(Swapper widget) : super(widget);
Element stable;
Element swapper;
bool swapperIsOnTop = true;
List<dynamic> insertSlots = <dynamic>[];
List<Pair<dynamic>> moveSlots = <Pair<dynamic>>[];
List<dynamic> removeSlots = <dynamic>[];
@override
Swapper get widget => super.widget as Swapper;
@override
RenderSwapper get renderObject => super.renderObject as RenderSwapper;
@override
void visitChildren(ElementVisitor visitor) {
if (stable != null)
visitor(stable);
if (swapper != null)
visitor(swapper);
}
@override
void update(Swapper newWidget) {
super.update(newWidget);
_updateChildren(newWidget);
}
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_updateChildren(widget);
}
void _updateChildren(Swapper widget) {
stable = updateChild(stable, widget.stable, 'stable');
swapper = updateChild(swapper, widget.swapper, swapperIsOnTop);
swapperIsOnTop = !swapperIsOnTop;
}
}
class SwapperElementWithProperOverrides extends SwapperElement {
SwapperElementWithProperOverrides(Swapper widget) : super(widget);
@override
void insertRenderObjectChild(RenderBox child, dynamic slot) {
insertSlots.add(slot);
assert(child != null);
if (slot == 'stable')
renderObject.stable = child;
else
renderObject.setSwapper(child, slot as bool);
}
@override
void moveRenderObjectChild(RenderBox child, bool oldIsOnTop, bool newIsOnTop) {
moveSlots.add(Pair<bool>(oldIsOnTop, newIsOnTop));
assert(oldIsOnTop == !newIsOnTop);
renderObject.setSwapper(child, newIsOnTop);
}
@override
void removeRenderObjectChild(RenderBox child, dynamic slot) {
removeSlots.add(slot);
if (slot == 'stable')
renderObject.stable = null;
else
renderObject.setSwapper(null, slot as bool);
}
}
class SwapperElementWithNoOverrides extends SwapperElement {
SwapperElementWithNoOverrides(Swapper widget) : super(widget);
}
class SwapperElementWithDeprecatedOverrides extends SwapperElement {
SwapperElementWithDeprecatedOverrides(Swapper widget) : super(widget);
@override
// ignore: must_call_super
void insertChildRenderObject(RenderBox child, dynamic slot) {
insertSlots.add(slot);
assert(child != null);
if (slot == 'stable')
renderObject.stable = child;
else
renderObject.setSwapper(child, slot as bool);
}
@override
// ignore: must_call_super
void moveChildRenderObject(RenderBox child, bool isOnTop) {
moveSlots.add(Pair<bool>(null, isOnTop));
renderObject.setSwapper(child, isOnTop);
}
@override
// ignore: must_call_super
void removeChildRenderObject(RenderBox child) {
removeSlots.add(null);
if (child == renderObject._stable)
renderObject.stable = null;
else
renderObject.setSwapper(null, swapperIsOnTop);
}
}
class RenderSwapper extends RenderBox {
RenderBox _stable;
RenderBox get stable => _stable;
set stable(RenderBox child) {
if (child == _stable)
return;
if (_stable != null)
dropChild(_stable);
_stable = child;
if (child != null)
adoptChild(child);
}
bool _swapperIsOnTop;
RenderBox _swapper;
RenderBox get swapper => _swapper;
void setSwapper(RenderBox child, bool isOnTop) {
if (isOnTop != _swapperIsOnTop) {
_swapperIsOnTop = isOnTop;
markNeedsLayout();
}
if (child == _swapper)
return;
if (_swapper != null)
dropChild(_swapper);
_swapper = child;
if (child != null)
adoptChild(child);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
if (_stable != null)
visitor(_stable);
if (_swapper != null)
visitor(_swapper);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
visitChildren((RenderObject child) => child.attach(owner));
}
@override
void detach() {
super.detach();
visitChildren((RenderObject child) => child.detach());
}
@override
void performLayout() {
assert(constraints.hasBoundedWidth);
assert(constraints.hasTightHeight);
size = constraints.biggest;
const Offset topOffset = Offset.zero;
final Offset bottomOffset = Offset(0, size.height / 2);
final BoxConstraints childConstraints = constraints.copyWith(
minHeight: constraints.minHeight / 2,
maxHeight: constraints.maxHeight / 2,
);
if (_stable != null) {
final BoxParentData stableParentData = _stable.parentData as BoxParentData;
_stable.layout(childConstraints);
stableParentData.offset = _swapperIsOnTop ? bottomOffset : topOffset;
}
if (_swapper != null) {
final BoxParentData swapperParentData = _swapper.parentData as BoxParentData;
_swapper.layout(childConstraints);
swapperParentData.offset = _swapperIsOnTop ? topOffset : bottomOffset;
}
}
@override
void paint(PaintingContext context, Offset offset) {
visitChildren((RenderObject child) {
final BoxParentData childParentData = child.parentData as BoxParentData;
context.paintChild(child, offset + childParentData.offset);
});
}
@override
void redepthChildren() {
visitChildren((RenderObject child) => redepthChild(child));
}
}
BoxParentData parentDataFor(RenderObject renderObject) => renderObject.parentData as BoxParentData;
void main() {
testWidgets('RenderObjectElement *RenderObjectChild methods get called with correct arguments', (WidgetTester tester) async {
const Key redKey = ValueKey<String>('red');
const Key blueKey = ValueKey<String>('blue');
Widget widget() {
return SwapperWithProperOverrides(
stable: ColoredBox(
key: redKey,
color: Color(nonconst(0xffff0000)),
),
swapper: ColoredBox(
key: blueKey,
color: Color(nonconst(0xff0000ff)),
),
);
}
await tester.pumpWidget(widget());
final SwapperElement swapper = tester.element<SwapperElement>(find.byType(SwapperWithProperOverrides));
final RenderBox redBox = tester.renderObject<RenderBox>(find.byKey(redKey));
final RenderBox blueBox = tester.renderObject<RenderBox>(find.byKey(blueKey));
expect(swapper.insertSlots.length, 2);
expect(swapper.insertSlots, contains('stable'));
expect(swapper.insertSlots, contains(true));
expect(swapper.moveSlots, isEmpty);
expect(swapper.removeSlots, isEmpty);
expect(parentDataFor(redBox).offset, const Offset(0, 300));
expect(parentDataFor(blueBox).offset, Offset.zero);
await tester.pumpWidget(widget());
expect(swapper.insertSlots.length, 2);
expect(swapper.moveSlots.length, 1);
expect(swapper.moveSlots, contains(const Pair<bool>(true, false)));
expect(swapper.removeSlots, isEmpty);
expect(parentDataFor(redBox).offset, Offset.zero);
expect(parentDataFor(blueBox).offset, const Offset(0, 300));
await tester.pumpWidget(const SwapperWithProperOverrides());
expect(redBox.attached, false);
expect(blueBox.attached, false);
expect(swapper.insertSlots.length, 2);
expect(swapper.moveSlots.length, 1);
expect(swapper.removeSlots.length, 2);
expect(swapper.removeSlots, contains('stable'));
expect(swapper.removeSlots, contains(false));
});
testWidgets('RenderObjectElement *RenderObjectChild methods delegate to deprecated methods', (WidgetTester tester) async {
const Key redKey = ValueKey<String>('red');
const Key blueKey = ValueKey<String>('blue');
Widget widget() {
return SwapperWithDeprecatedOverrides(
stable: ColoredBox(
key: redKey,
color: Color(nonconst(0xffff0000)),
),
swapper: ColoredBox(
key: blueKey,
color: Color(nonconst(0xff0000ff)),
),
);
}
await tester.pumpWidget(widget());
final SwapperElement swapper = tester.element<SwapperElement>(find.byType(SwapperWithDeprecatedOverrides));
final RenderBox redBox = tester.renderObject<RenderBox>(find.byKey(redKey));
final RenderBox blueBox = tester.renderObject<RenderBox>(find.byKey(blueKey));
expect(swapper.insertSlots.length, 2);
expect(swapper.insertSlots, contains('stable'));
expect(swapper.insertSlots, contains(true));
expect(swapper.moveSlots, isEmpty);
expect(swapper.removeSlots, isEmpty);
expect(parentDataFor(redBox).offset, const Offset(0, 300));
expect(parentDataFor(blueBox).offset, Offset.zero);
await tester.pumpWidget(widget());
expect(swapper.insertSlots.length, 2);
expect(swapper.moveSlots.length, 1);
expect(swapper.moveSlots, contains(const Pair<bool>(null, false)));
expect(swapper.removeSlots, isEmpty);
expect(parentDataFor(redBox).offset, Offset.zero);
expect(parentDataFor(blueBox).offset, const Offset(0, 300));
await tester.pumpWidget(const SwapperWithDeprecatedOverrides());
expect(redBox.attached, false);
expect(blueBox.attached, false);
expect(swapper.insertSlots.length, 2);
expect(swapper.moveSlots.length, 1);
expect(swapper.removeSlots.length, 2);
expect(swapper.removeSlots, <bool>[null,null]);
});
testWidgets('RenderObjectElement *ChildRenderObject methods fail with deprecation message', (WidgetTester tester) async {
const Key redKey = ValueKey<String>('red');
const Key blueKey = ValueKey<String>('blue');
Widget widget() {
return SwapperWithNoOverrides(
stable: ColoredBox(
key: redKey,
color: Color(nonconst(0xffff0000)),
),
swapper: ColoredBox(
key: blueKey,
color: Color(nonconst(0xff0000ff)),
),
);
}
await tester.pumpWidget(widget());
final FlutterError error = tester.takeException() as FlutterError;
final ErrorSummary summary = error.diagnostics.first as ErrorSummary;
expect(summary.toString(), contains('deprecated'));
});
}