// 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 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; const Color green = Color(0xFF00FF00); const Color yellow = Color(0xFFFFFF00); void main() { testWidgets('SlottedRenderObjectWidget test', (WidgetTester tester) async { await tester.pumpWidget(buildWidget( topLeft: Container( height: 100, width: 80, color: yellow, child: const Text('topLeft'), ), bottomRight: Container( height: 120, width: 110, color: green, child: const Text('bottomRight'), ), )); expect(find.text('topLeft'), findsOneWidget); expect(find.text('bottomRight'), findsOneWidget); expect(tester.getSize(find.byType(_Diagonal)), const Size(80 + 110, 100 + 120)); expect(find.byType(_Diagonal), paints ..rect( rect: const Rect.fromLTWH(0, 0, 80, 100), color: yellow, ) ..rect( rect: const Rect.fromLTWH(80, 100, 110, 120), color: green, ) ); await tester.pumpWidget(buildWidget( topLeft: Container( height: 200, width: 100, color: yellow, child: const Text('topLeft'), ), bottomRight: Container( height: 220, width: 210, color: green, child: const Text('bottomRight'), ), )); expect(find.text('topLeft'), findsOneWidget); expect(find.text('bottomRight'), findsOneWidget); expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 210, 200 + 220)); expect(find.byType(_Diagonal), paints ..rect( rect: const Rect.fromLTWH(0, 0, 100, 200), color: yellow, ) ..rect( rect: const Rect.fromLTWH(100, 200, 210, 220), color: green, ) ); await tester.pumpWidget(buildWidget( topLeft: Container( height: 200, width: 100, color: yellow, child: const Text('topLeft'), ), bottomRight: Container( key: UniqueKey(), height: 230, width: 220, color: green, child: const Text('bottomRight'), ), )); expect(find.text('topLeft'), findsOneWidget); expect(find.text('bottomRight'), findsOneWidget); expect(tester.getSize(find.byType(_Diagonal)), const Size(100 + 220, 200 + 230)); expect(find.byType(_Diagonal), paints ..rect( rect: const Rect.fromLTWH(0, 0, 100, 200), color: yellow, ) ..rect( rect: const Rect.fromLTWH(100, 200, 220, 230), color: green, ) ); await tester.pumpWidget(buildWidget( topLeft: Container( height: 200, width: 100, color: yellow, child: const Text('topLeft'), ), )); expect(find.text('topLeft'), findsOneWidget); expect(find.text('bottomRight'), findsNothing); expect(tester.getSize(find.byType(_Diagonal)), const Size(100, 200)); expect(find.byType(_Diagonal), paints ..rect( rect: const Rect.fromLTWH(0, 0, 100, 200), color: yellow, ) ); await tester.pumpWidget(buildWidget()); expect(find.text('topLeft'), findsNothing); expect(find.text('bottomRight'), findsNothing); expect(tester.getSize(find.byType(_Diagonal)), Size.zero); expect(find.byType(_Diagonal), paintsNothing); await tester.pumpWidget(Container()); expect(find.byType(_Diagonal), findsNothing); }); test('nameForSlot', () { expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.bottomRight), 'bottomRight'); expect(_RenderDiagonal().publicNameForSlot(_DiagonalSlot.topLeft), 'topLeft'); final _Slot slot = _Slot(); expect(_RenderTest().publicNameForSlot(slot), slot.toString()); }); testWidgets('key reparenting', (WidgetTester tester) async { const Widget widget1 = SizedBox(key: ValueKey<String>('smol'), height: 10, width: 10); const Widget widget2 = SizedBox(key: ValueKey<String>('big'), height: 100, width: 100); const Widget nullWidget = SizedBox(key: ValueKey<String>('null'), height: 50, width: 50); await tester.pumpWidget(buildWidget(topLeft: widget1, bottomRight: widget2, nullSlot: nullWidget)); final _RenderDiagonal renderObject = tester.renderObject(find.byType(_Diagonal)); expect(renderObject._topLeft!.size, const Size(10, 10)); expect(renderObject._bottomRight!.size, const Size(100, 100)); expect(renderObject._nullSlot!.size, const Size(50, 50)); final Element widget1Element = tester.element(find.byWidget(widget1)); final Element widget2Element = tester.element(find.byWidget(widget2)); final Element nullWidgetElement = tester.element(find.byWidget(nullWidget)); // Swapping 1 and 2. await tester.pumpWidget(buildWidget(topLeft: widget2, bottomRight: widget1, nullSlot: nullWidget)); expect(renderObject._topLeft!.size, const Size(100, 100)); expect(renderObject._bottomRight!.size, const Size(10, 10)); expect(renderObject._nullSlot!.size, const Size(50, 50)); expect(widget1Element, same(tester.element(find.byWidget(widget1)))); expect(widget2Element, same(tester.element(find.byWidget(widget2)))); expect(nullWidgetElement, same(tester.element(find.byWidget(nullWidget)))); // Shifting slots await tester.pumpWidget(buildWidget(topLeft: nullWidget, bottomRight: widget2, nullSlot: widget1)); expect(renderObject._topLeft!.size, const Size(50, 50)); expect(renderObject._bottomRight!.size, const Size(100, 100)); expect(renderObject._nullSlot!.size, const Size(10, 10)); expect(widget1Element, same(tester.element(find.byWidget(widget1)))); expect(widget2Element, same(tester.element(find.byWidget(widget2)))); expect(nullWidgetElement, same(tester.element(find.byWidget(nullWidget)))); // Moving + Deleting. await tester.pumpWidget(buildWidget(bottomRight: widget2)); expect(renderObject._topLeft, null); expect(renderObject._bottomRight!.size, const Size(100, 100)); expect(renderObject._nullSlot, null); expect(widget1Element.debugIsDefunct, isTrue); expect(nullWidgetElement.debugIsDefunct, isTrue); expect(widget2Element, same(tester.element(find.byWidget(widget2)))); // Moving. await tester.pumpWidget(buildWidget(topLeft: widget2)); expect(renderObject._topLeft!.size, const Size(100, 100)); expect(renderObject._bottomRight, null); expect(widget2Element, same(tester.element(find.byWidget(widget2)))); }); testWidgets('duplicated key error message', (WidgetTester tester) async { const Widget widget1 = SizedBox(key: ValueKey<String>('widget 1'), height: 10, width: 10); const Widget widget2 = SizedBox(key: ValueKey<String>('widget 1'), height: 100, width: 100); const Widget widget3 = SizedBox(key: ValueKey<String>('widget 1'), height: 50, width: 50); await tester.pumpWidget(buildWidget(topLeft: widget1, bottomRight: widget2, nullSlot: widget3)); expect((tester.takeException() as FlutterError).toString(), equalsIgnoringHashCodes( 'Multiple widgets used the same key in _Diagonal.\n' "The key [<'widget 1'>] was used by multiple widgets. The offending widgets were:\n" " - SizedBox-[<'widget 1'>](width: 50.0, height: 50.0, renderObject: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT)\n" " - SizedBox-[<'widget 1'>](width: 10.0, height: 10.0, renderObject: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT)\n" " - SizedBox-[<'widget 1'>](width: 100.0, height: 100.0, renderObject: RenderConstrainedBox#a4685 NEEDS-LAYOUT NEEDS-PAINT)\n" 'A key can only be specified on one widget at a time in the same parent widget.' )); }); testWidgets('debugDescribeChildren', (WidgetTester tester) async { await tester.pumpWidget(buildWidget( topLeft: const SizedBox( height: 100, width: 80, ), bottomRight: const SizedBox( height: 120, width: 110, ), )); expect( tester.renderObject(find.byType(_Diagonal)).toStringDeep(), equalsIgnoringHashCodes( '_RenderDiagonal#00000 relayoutBoundary=up1\n' ' │ creator: _Diagonal ← Align ← Directionality ← MediaQuery ←\n' ' │ _MediaQueryFromView ← _ViewScope ← View-[GlobalObjectKey\n' ' │ TestFlutterView#00000] ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(0.0<=w<=800.0, 0.0<=h<=600.0)\n' ' │ size: Size(190.0, 220.0)\n' ' │\n' ' ├─topLeft: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' │ creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' ' │ MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' │ View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' │ parentData: offset=Offset(0.0, 0.0) (can use size)\n' ' │ constraints: BoxConstraints(unconstrained)\n' ' │ size: Size(80.0, 100.0)\n' ' │ additionalConstraints: BoxConstraints(w=80.0, h=100.0)\n' ' │\n' ' └─bottomRight: RenderConstrainedBox#00000 relayoutBoundary=up2\n' ' creator: SizedBox ← _Diagonal ← Align ← Directionality ←\n' ' MediaQuery ← _MediaQueryFromView ← _ViewScope ←\n' ' View-[GlobalObjectKey TestFlutterView#00000] ← [root]\n' ' parentData: offset=Offset(80.0, 100.0) (can use size)\n' ' constraints: BoxConstraints(unconstrained)\n' ' size: Size(110.0, 120.0)\n' ' additionalConstraints: BoxConstraints(w=110.0, h=120.0)\n', ) ); }); } Widget buildWidget({Widget? topLeft, Widget? bottomRight, Widget? nullSlot}) { return Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: _Diagonal( topLeft: topLeft, bottomRight: bottomRight, nullSlot: nullSlot, ), ), ); } enum _DiagonalSlot { topLeft, bottomRight, } class _Diagonal extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_DiagonalSlot?> { const _Diagonal({ this.topLeft, this.bottomRight, this.nullSlot, }); final Widget? topLeft; final Widget? bottomRight; final Widget? nullSlot; @override Iterable<_DiagonalSlot?> get slots => <_DiagonalSlot?>[null, ..._DiagonalSlot.values]; @override Widget? childForSlot(_DiagonalSlot? slot) { switch (slot) { case null: return nullSlot; case _DiagonalSlot.topLeft: return topLeft; case _DiagonalSlot.bottomRight: return bottomRight; } } @override SlottedContainerRenderObjectMixin<_DiagonalSlot?> createRenderObject( BuildContext context, ) { return _RenderDiagonal(); } } class _RenderDiagonal extends RenderBox with SlottedContainerRenderObjectMixin<_DiagonalSlot?> { RenderBox? get _topLeft => childForSlot(_DiagonalSlot.topLeft); RenderBox? get _bottomRight => childForSlot(_DiagonalSlot.bottomRight); RenderBox? get _nullSlot => childForSlot(null); @override void performLayout() { const BoxConstraints childConstraints = BoxConstraints(); Size topLeftSize = Size.zero; if (_topLeft != null) { _topLeft!.layout(childConstraints, parentUsesSize: true); _positionChild(_topLeft!, Offset.zero); topLeftSize = _topLeft!.size; } Size bottomRightSize = Size.zero; if (_bottomRight != null) { _bottomRight!.layout(childConstraints, parentUsesSize: true); _positionChild( _bottomRight!, Offset(topLeftSize.width, topLeftSize.height), ); bottomRightSize = _bottomRight!.size; } Size nullSlotSize = Size.zero; final RenderBox? nullSlot = _nullSlot; if (nullSlot != null) { nullSlot.layout(childConstraints, parentUsesSize: true); _positionChild(nullSlot, Offset.zero); nullSlotSize = nullSlot.size; } size = constraints.constrain(Size( topLeftSize.width + bottomRightSize.width + nullSlotSize.width, topLeftSize.height + bottomRightSize.height + nullSlotSize.height, )); } void _positionChild(RenderBox child, Offset offset) { (child.parentData! as BoxParentData).offset = offset; } @override void paint(PaintingContext context, Offset offset) { if (_topLeft != null) { _paintChild(_topLeft!, context, offset); } if (_bottomRight != null) { _paintChild(_bottomRight!, context, offset); } } void _paintChild(RenderBox child, PaintingContext context, Offset offset) { final BoxParentData childParentData = child.parentData! as BoxParentData; context.paintChild(child, childParentData.offset + offset); } String publicNameForSlot(_DiagonalSlot slot) => debugNameForSlot(slot); } class _Slot { @override String toString() => describeIdentity(this); } class _RenderTest extends RenderBox with SlottedContainerRenderObjectMixin<_Slot> { String publicNameForSlot(_Slot slot) => debugNameForSlot(slot); }