// 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/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'rendering_tester.dart'; class MissingPerformLayoutRenderBox extends RenderBox { void triggerExceptionSettingSizeOutsideOfLayout() { size = const Size(200, 200); } // performLayout is left unimplemented to test the error reported if it is // missing. } class FakeMissingSizeRenderBox extends RenderBox { @override void performLayout() { size = constraints.biggest; } @override bool get hasSize => !fakeMissingSize && super.hasSize; bool fakeMissingSize = false; } class MissingSetSizeRenderBox extends RenderBox { @override void performLayout() { } } class BadBaselineRenderBox extends RenderBox { @override void performLayout() { size = constraints.biggest; } @override double? computeDistanceToActualBaseline(TextBaseline baseline) { throw Exception(); } } void main() { TestRenderingFlutterBinding.ensureInitialized(); test('should size to render view', () { final RenderBox root = RenderDecoratedBox( decoration: BoxDecoration( color: const Color(0xFF00FF00), gradient: RadialGradient( center: Alignment.topLeft, radius: 1.8, colors: <Color>[Colors.yellow[500]!, Colors.blue[500]!], ), boxShadow: kElevationToShadow[3], ), ); layout(root); expect(root.size.width, equals(800.0)); expect(root.size.height, equals(600.0)); }); test('performLayout error message', () { late FlutterError result; try { MissingPerformLayoutRenderBox().performLayout(); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' MissingPerformLayoutRenderBox did not implement performLayout().\n' ' RenderBox subclasses need to either override performLayout() to\n' ' set a size and lay out any children, or, set sizedByParent to\n' ' true so that performResize() sizes the render object.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'RenderBox subclasses need to either override performLayout() to set a ' 'size and lay out any children, or, set sizedByParent to true so that ' 'performResize() sizes the render object.', ); }); test('applyPaintTransform error message', () { final RenderBox paddingBox = RenderPadding( padding: const EdgeInsets.all(10.0), ); final RenderBox root = RenderPadding( padding: const EdgeInsets.all(10.0), child: paddingBox, ); layout(root); // Trigger the error by overriding the parentData with data that isn't a // BoxParentData. paddingBox.parentData = ParentData(); late FlutterError result; try { root.applyPaintTransform(paddingBox, Matrix4.identity()); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' RenderPadding does not implement applyPaintTransform.\n' ' The following RenderPadding object: RenderPadding#00000 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:\n' ' parentData: <none>\n' ' constraints: BoxConstraints(w=800.0, h=600.0)\n' ' size: Size(800.0, 600.0)\n' ' padding: EdgeInsets.all(10.0)\n' ' ...did not use a BoxParentData class for the parentData field of the following child:\n' ' RenderPadding#00000 NEEDS-PAINT:\n' ' parentData: <none> (can use size)\n' ' constraints: BoxConstraints(w=780.0, h=580.0)\n' ' size: Size(780.0, 580.0)\n' ' padding: EdgeInsets.all(10.0)\n' ' The RenderPadding class inherits from RenderBox.\n' ' The default applyPaintTransform implementation provided by\n' ' RenderBox assumes that the children all use BoxParentData objects\n' ' for their parentData field. Since RenderPadding does not in fact\n' ' use that ParentData class for its children, it must provide an\n' ' implementation of applyPaintTransform that supports the specific\n' ' ParentData subclass used by its children (which apparently is\n' ' ParentData).\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'The default applyPaintTransform implementation provided by RenderBox ' 'assumes that the children all use BoxParentData objects for their ' 'parentData field. Since RenderPadding does not in fact use that ' 'ParentData class for its children, it must provide an implementation ' 'of applyPaintTransform that supports the specific ParentData subclass ' 'used by its children (which apparently is ParentData).', ); }); test('Set size error messages', () { final RenderBox root = RenderDecoratedBox( decoration: const BoxDecoration( color: Color(0xFF00FF00), ), ); layout(root); final MissingPerformLayoutRenderBox testBox = MissingPerformLayoutRenderBox(); { late FlutterError result; try { testBox.triggerExceptionSettingSizeOutsideOfLayout(); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' RenderBox size setter called incorrectly.\n' ' The size setter was called from outside layout (neither\n' ' performResize() nor performLayout() were being run for this\n' ' object).\n' ' Because this RenderBox has sizedByParent set to false, it must\n' ' set its size in performLayout().\n', ), ); expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint), isEmpty); } { late FlutterError result; try { testBox.debugAdoptSize(root.size); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The size property was assigned a size inappropriately.\n' ' The following render object: MissingPerformLayoutRenderBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n' ' parentData: MISSING\n' ' constraints: MISSING\n' ' size: MISSING\n' ' ...was assigned a size obtained from: RenderDecoratedBox#00000 NEEDS-PAINT:\n' ' parentData: <none>\n' ' constraints: BoxConstraints(w=800.0, h=600.0)\n' ' size: Size(800.0, 600.0)\n' ' decoration: BoxDecoration:\n' ' color: Color(0xff00ff00)\n' ' configuration: ImageConfiguration()\n' ' However, this second render object is not, or is no longer, a\n' ' child of the first, and it is therefore a violation of the\n' ' RenderBox layout protocol to use that size in the layout of the\n' ' first render object.\n' ' If the size was obtained at a time where it was valid to read the\n' ' size (because the second render object above was a child of the\n' ' first at the time), then it should be adopted using\n' ' debugAdoptSize at that time.\n' ' If the size comes from a grandchild or a render object from an\n' ' entirely different part of the render tree, then there is no way\n' ' to be notified when the size changes and therefore attempts to\n' ' read that size are almost certainly a source of bugs. A different\n' ' approach should be used.\n', ), ); expect(result.diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).length, 2); } }); test('Flex and padding', () { final RenderBox size = RenderConstrainedBox( additionalConstraints: const BoxConstraints().tighten(height: 100.0), ); final RenderBox inner = RenderDecoratedBox( decoration: const BoxDecoration( color: Color(0xFF00FF00), ), child: size, ); final RenderBox padding = RenderPadding( padding: const EdgeInsets.all(50.0), child: inner, ); final RenderBox flex = RenderFlex( children: <RenderBox>[padding], direction: Axis.vertical, crossAxisAlignment: CrossAxisAlignment.stretch, ); final RenderBox outer = RenderDecoratedBox( decoration: const BoxDecoration( color: Color(0xFF0000FF), ), child: flex, ); layout(outer); expect(size.size.width, equals(700.0)); expect(size.size.height, equals(100.0)); expect(inner.size.width, equals(700.0)); expect(inner.size.height, equals(100.0)); expect(padding.size.width, equals(800.0)); expect(padding.size.height, equals(200.0)); expect(flex.size.width, equals(800.0)); expect(flex.size.height, equals(600.0)); expect(outer.size.width, equals(800.0)); expect(outer.size.height, equals(600.0)); }); test('should not have a 0 sized colored Box', () { final RenderBox coloredBox = RenderDecoratedBox( decoration: const BoxDecoration(), ); expect(coloredBox, hasAGoodToStringDeep); expect( coloredBox.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( 'RenderDecoratedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n' ' parentData: MISSING\n' ' constraints: MISSING\n' ' size: MISSING\n' ' decoration: BoxDecoration:\n' ' <no decorations specified>\n' ' configuration: ImageConfiguration()\n', ), ); final RenderBox paddingBox = RenderPadding( padding: const EdgeInsets.all(10.0), child: coloredBox, ); final RenderBox root = RenderDecoratedBox( decoration: const BoxDecoration(), child: paddingBox, ); layout(root); expect(coloredBox.size.width, equals(780.0)); expect(coloredBox.size.height, equals(580.0)); expect(coloredBox, hasAGoodToStringDeep); expect( coloredBox.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( 'RenderDecoratedBox#00000 NEEDS-PAINT\n' ' parentData: offset=Offset(10.0, 10.0) (can use size)\n' ' constraints: BoxConstraints(w=780.0, h=580.0)\n' ' size: Size(780.0, 580.0)\n' ' decoration: BoxDecoration:\n' ' <no decorations specified>\n' ' configuration: ImageConfiguration()\n', ), ); }); test('reparenting should clear position', () { final RenderDecoratedBox coloredBox = RenderDecoratedBox( decoration: const BoxDecoration(), ); final RenderPadding paddedBox = RenderPadding( child: coloredBox, padding: const EdgeInsets.all(10.0), ); layout(paddedBox); final BoxParentData parentData = coloredBox.parentData! as BoxParentData; expect(parentData.offset.dx, isNot(equals(0.0))); paddedBox.child = null; final RenderConstrainedBox constrainedBox = RenderConstrainedBox( child: coloredBox, additionalConstraints: const BoxConstraints(), ); layout(constrainedBox); expect(coloredBox.parentData?.runtimeType, ParentData); }); test('UnconstrainedBox expands to fit children', () { final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.widthUnconstrained, textDirection: TextDirection.ltr, child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(width: 200.0, height: 200.0), ), alignment: Alignment.center, ); layout( unconstrained, constraints: const BoxConstraints( minWidth: 200.0, maxWidth: 200.0, minHeight: 200.0, maxHeight: 200.0, ), ); // Check that we can update the constrained axis to null. unconstrained.constraintsTransform = ConstraintsTransformBox.unconstrained; TestRenderingFlutterBinding.instance.reassembleApplication(); expect(unconstrained.size.width, equals(200.0), reason: 'unconstrained width'); expect(unconstrained.size.height, equals(200.0), reason: 'unconstrained height'); }); test('UnconstrainedBox handles vertical overflow', () { final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, textDirection: TextDirection.ltr, child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(height: 200.0), ), alignment: Alignment.center, ); const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0); layout(unconstrained, constraints: viewport); expect(unconstrained.getMinIntrinsicHeight(100.0), equals(200.0)); expect(unconstrained.getMaxIntrinsicHeight(100.0), equals(200.0)); expect(unconstrained.getMinIntrinsicWidth(100.0), equals(0.0)); expect(unconstrained.getMaxIntrinsicWidth(100.0), equals(0.0)); }); test('UnconstrainedBox handles horizontal overflow', () { final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, textDirection: TextDirection.ltr, child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(width: 200.0), ), alignment: Alignment.center, ); const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0); layout(unconstrained, constraints: viewport); expect(unconstrained.getMinIntrinsicHeight(100.0), equals(0.0)); expect(unconstrained.getMaxIntrinsicHeight(100.0), equals(0.0)); expect(unconstrained.getMinIntrinsicWidth(100.0), equals(200.0)); expect(unconstrained.getMaxIntrinsicWidth(100.0), equals(200.0)); }); group('ConstraintsTransformBox', () { FlutterErrorDetails? firstErrorDetails; void exhaustErrors() { FlutterErrorDetails? next; do { next = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails(); firstErrorDetails ??= next; } while (next != null); } tearDown(() { firstErrorDetails = null; RenderObject.debugCheckingIntrinsics = false; }); test('throws if the resulting constraints are not normalized', () { final RenderConstrainedBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(height: 0)); final RenderConstraintsTransformBox box = RenderConstraintsTransformBox( alignment: Alignment.center, textDirection: TextDirection.ltr, constraintsTransform: (BoxConstraints constraints) => const BoxConstraints(maxHeight: -1, minHeight: 200), child: child, ); layout(box, constraints: const BoxConstraints(), onErrors: exhaustErrors); expect(firstErrorDetails?.toString(), contains('is not normalized')); }); test('overflow is reported when insufficient size is given and clipBehavior is Clip.none', () { bool hadErrors = false; void expectOverflowedErrors() { absorbOverflowedErrors(); hadErrors = true; } final TestClipPaintingContext context = TestClipPaintingContext(); for (final Clip? clip in <Clip?>[null, ...Clip.values]) { final RenderConstraintsTransformBox box; switch (clip) { case Clip.none: case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: box = RenderConstraintsTransformBox( alignment: Alignment.center, textDirection: TextDirection.ltr, constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity), clipBehavior: clip!, child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor( width: double.maxFinite, height: double.maxFinite, ), ), ); case null: box = RenderConstraintsTransformBox( alignment: Alignment.center, textDirection: TextDirection.ltr, constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity), child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor( width: double.maxFinite, height: double.maxFinite, ), ), ); } layout(box, constraints: const BoxConstraints(), phase: EnginePhase.composite, onErrors: expectOverflowedErrors); context.paintChild(box, Offset.zero); // By default, clipBehavior should be Clip.none expect(context.clipBehavior, equals(clip ?? Clip.none)); switch (clip) { case null: case Clip.none: expect(hadErrors, isTrue, reason: 'Should have had overflow errors for $clip'); case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip'); } hadErrors = false; } }); test('handles flow layout', () { final RenderParagraph child = RenderParagraph( TextSpan(text: 'a' * 100), textDirection: TextDirection.ltr, ); final RenderConstraintsTransformBox box = RenderConstraintsTransformBox( alignment: Alignment.center, textDirection: TextDirection.ltr, constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity), child: child, ); // With a width of 30, the RenderParagraph would have wrapped, but the // RenderConstraintsTransformBox allows the paragraph to expand regardless // of the width constraint: // unconstrainedHeight * numberOfLines = constrainedHeight. final double constrainedHeight = child.getMinIntrinsicHeight(30); final double unconstrainedHeight = box.getMinIntrinsicHeight(30); // At least 2 lines. expect(constrainedHeight, greaterThanOrEqualTo(2 * unconstrainedHeight)); }); }); test ('getMinIntrinsicWidth error handling', () { final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, textDirection: TextDirection.ltr, child: RenderConstrainedBox( additionalConstraints: const BoxConstraints.tightFor(width: 200.0), ), alignment: Alignment.center, ); const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0); layout(unconstrained, constraints: viewport); { late FlutterError result; try { unconstrained.getMinIntrinsicWidth(-1); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The height argument to getMinIntrinsicWidth was negative.\n' ' The argument to getMinIntrinsicWidth must not be negative or\n' ' null.\n' ' If you perform computations on another height before passing it\n' ' to getMinIntrinsicWidth, consider using math.max() or\n' ' double.clamp() to force the value into the valid range.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'If you perform computations on another height before passing it to ' 'getMinIntrinsicWidth, consider using math.max() or double.clamp() ' 'to force the value into the valid range.', ); } { late FlutterError result; try { unconstrained.getMinIntrinsicHeight(-1); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The width argument to getMinIntrinsicHeight was negative.\n' ' The argument to getMinIntrinsicHeight must not be negative or\n' ' null.\n' ' If you perform computations on another width before passing it to\n' ' getMinIntrinsicHeight, consider using math.max() or\n' ' double.clamp() to force the value into the valid range.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'If you perform computations on another width before passing it to ' 'getMinIntrinsicHeight, consider using math.max() or double.clamp() ' 'to force the value into the valid range.', ); } { late FlutterError result; try { unconstrained.getMaxIntrinsicWidth(-1); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The height argument to getMaxIntrinsicWidth was negative.\n' ' The argument to getMaxIntrinsicWidth must not be negative or\n' ' null.\n' ' If you perform computations on another height before passing it\n' ' to getMaxIntrinsicWidth, consider using math.max() or\n' ' double.clamp() to force the value into the valid range.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'If you perform computations on another height before passing it to ' 'getMaxIntrinsicWidth, consider using math.max() or double.clamp() ' 'to force the value into the valid range.', ); } { late FlutterError result; try { unconstrained.getMaxIntrinsicHeight(-1); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' The width argument to getMaxIntrinsicHeight was negative.\n' ' The argument to getMaxIntrinsicHeight must not be negative or\n' ' null.\n' ' If you perform computations on another width before passing it to\n' ' getMaxIntrinsicHeight, consider using math.max() or\n' ' double.clamp() to force the value into the valid range.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'If you perform computations on another width before passing it to ' 'getMaxIntrinsicHeight, consider using math.max() or double.clamp() ' 'to force the value into the valid range.', ); } }); test('UnconstrainedBox.toStringDeep returns useful information', () { final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, textDirection: TextDirection.ltr, alignment: Alignment.center, ); expect(unconstrained.alignment, Alignment.center); expect(unconstrained.textDirection, TextDirection.ltr); expect(unconstrained, hasAGoodToStringDeep); expect( unconstrained.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( 'RenderConstraintsTransformBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n' ' parentData: MISSING\n' ' constraints: MISSING\n' ' size: MISSING\n' ' alignment: Alignment.center\n' ' textDirection: ltr\n', ), ); }); test('UnconstrainedBox honors constrainedAxis=Axis.horizontal', () { final RenderConstrainedBox flexible = RenderConstrainedBox(additionalConstraints: const BoxConstraints.expand(height: 200.0)); final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.heightUnconstrained, textDirection: TextDirection.ltr, child: RenderFlex( textDirection: TextDirection.ltr, children: <RenderBox>[flexible], ), alignment: Alignment.center, ); final FlexParentData flexParentData = flexible.parentData! as FlexParentData; flexParentData.flex = 1; flexParentData.fit = FlexFit.tight; const BoxConstraints viewport = BoxConstraints(maxWidth: 100.0); layout(unconstrained, constraints: viewport); expect(unconstrained.size.width, equals(100.0), reason: 'constrained width'); expect(unconstrained.size.height, equals(200.0), reason: 'unconstrained height'); }); test('UnconstrainedBox honors constrainedAxis=Axis.vertical', () { final RenderConstrainedBox flexible = RenderConstrainedBox(additionalConstraints: const BoxConstraints.expand(width: 200.0)); final RenderConstraintsTransformBox unconstrained = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.widthUnconstrained, textDirection: TextDirection.ltr, child: RenderFlex( direction: Axis.vertical, textDirection: TextDirection.ltr, children: <RenderBox>[flexible], ), alignment: Alignment.center, ); final FlexParentData flexParentData = flexible.parentData! as FlexParentData; flexParentData.flex = 1; flexParentData.fit = FlexFit.tight; const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0); layout(unconstrained, constraints: viewport); expect(unconstrained.size.width, equals(200.0), reason: 'unconstrained width'); expect(unconstrained.size.height, equals(100.0), reason: 'constrained height'); }); test('clipBehavior is respected', () { const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0); final TestClipPaintingContext context = TestClipPaintingContext(); bool hadErrors = false; void expectOverflowedErrors() { absorbOverflowedErrors(); hadErrors = true; } for (final Clip? clip in <Clip?>[null, ...Clip.values]) { final RenderConstraintsTransformBox box; switch (clip) { case Clip.none: case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: box = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, alignment: Alignment.center, textDirection: TextDirection.ltr, child: box200x200, clipBehavior: clip!, ); case null: box = RenderConstraintsTransformBox( constraintsTransform: ConstraintsTransformBox.unconstrained, alignment: Alignment.center, textDirection: TextDirection.ltr, child: box200x200, ); } layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors); switch (clip) { case null: case Clip.none: expect(hadErrors, isTrue, reason: 'Should have had overflow errors for $clip'); case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip'); } hadErrors = false; context.paintChild(box, Offset.zero); // By default, clipBehavior should be Clip.none expect(context.clipBehavior, equals(clip ?? Clip.none), reason: 'for $clip'); } }); group('hit testing', () { test('BoxHitTestResult wrapping HitTestResult', () { final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget()); final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget()); final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget()); final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0); final HitTestResult wrapped = MyHitTestResult() ..publicPushTransform(transform); wrapped.add(entry1); expect(wrapped.path, equals(<HitTestEntry>[entry1])); expect(entry1.transform, transform); final BoxHitTestResult wrapping = BoxHitTestResult.wrap(wrapped); expect(wrapping.path, equals(<HitTestEntry>[entry1])); expect(wrapping.path, same(wrapped.path)); wrapping.add(entry2); expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2])); expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2])); expect(entry2.transform, transform); wrapped.add(entry3); expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3])); expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3])); expect(entry3.transform, transform); }); test('addWithPaintTransform', () { final BoxHitTestResult result = BoxHitTestResult(); final List<Offset> positions = <Offset>[]; bool isHit = result.addWithPaintTransform( transform: null, position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, Offset.zero); positions.clear(); isHit = result.addWithPaintTransform( transform: Matrix4.translationValues(20, 30, 0), position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, const Offset(-20.0, -30.0)); positions.clear(); const Offset position = Offset(3, 4); isHit = result.addWithPaintTransform( transform: null, position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return false; }, ); expect(isHit, isFalse); expect(positions.single, position); positions.clear(); isHit = result.addWithPaintTransform( transform: Matrix4.identity(), position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position); positions.clear(); isHit = result.addWithPaintTransform( transform: Matrix4.translationValues(20, 30, 0), position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position - const Offset(20, 30)); positions.clear(); isHit = result.addWithPaintTransform( transform: MatrixUtils.forceToPoint(position), // cannot be inverted position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isFalse); expect(positions, isEmpty); positions.clear(); }); test('addWithPaintOffset', () { final BoxHitTestResult result = BoxHitTestResult(); final List<Offset> positions = <Offset>[]; bool isHit = result.addWithPaintOffset( offset: null, position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, Offset.zero); positions.clear(); isHit = result.addWithPaintOffset( offset: const Offset(55, 32), position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, const Offset(-55.0, -32.0)); positions.clear(); const Offset position = Offset(3, 4); isHit = result.addWithPaintOffset( offset: null, position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return false; }, ); expect(isHit, isFalse); expect(positions.single, position); positions.clear(); isHit = result.addWithPaintOffset( offset: Offset.zero, position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position); positions.clear(); isHit = result.addWithPaintOffset( offset: const Offset(20, 30), position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position - const Offset(20, 30)); positions.clear(); }); test('addWithRawTransform', () { final BoxHitTestResult result = BoxHitTestResult(); final List<Offset> positions = <Offset>[]; bool isHit = result.addWithRawTransform( transform: null, position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, Offset.zero); positions.clear(); isHit = result.addWithRawTransform( transform: Matrix4.translationValues(20, 30, 0), position: Offset.zero, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, const Offset(20.0, 30.0)); positions.clear(); const Offset position = Offset(3, 4); isHit = result.addWithRawTransform( transform: null, position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return false; }, ); expect(isHit, isFalse); expect(positions.single, position); positions.clear(); isHit = result.addWithRawTransform( transform: Matrix4.identity(), position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position); positions.clear(); isHit = result.addWithRawTransform( transform: Matrix4.translationValues(20, 30, 0), position: position, hitTest: (BoxHitTestResult result, Offset position) { expect(result, isNotNull); positions.add(position); return true; }, ); expect(isHit, isTrue); expect(positions.single, position + const Offset(20, 30)); positions.clear(); }); test('addWithOutOfBandPosition', () { final BoxHitTestResult result = BoxHitTestResult(); bool ran = false; bool isHit = result.addWithOutOfBandPosition( paintOffset: const Offset(20, 30), hitTest: (BoxHitTestResult result) { expect(result, isNotNull); ran = true; return true; }, ); expect(isHit, isTrue); expect(ran, isTrue); ran = false; isHit = result.addWithOutOfBandPosition( paintTransform: Matrix4.translationValues(20, 30, 0), hitTest: (BoxHitTestResult result) { expect(result, isNotNull); ran = true; return true; }, ); expect(isHit, isTrue); expect(ran, isTrue); ran = false; isHit = result.addWithOutOfBandPosition( rawTransform: Matrix4.translationValues(20, 30, 0), hitTest: (BoxHitTestResult result) { expect(result, isNotNull); ran = true; return true; }, ); expect(isHit, isTrue); expect(ran, isTrue); ran = false; isHit = result.addWithOutOfBandPosition( rawTransform: MatrixUtils.forceToPoint(Offset.zero), // cannot be inverted hitTest: (BoxHitTestResult result) { expect(result, isNotNull); ran = true; return true; }, ); expect(isHit, isTrue); expect(ran, isTrue); isHit = false; ran = false; expect( () { isHit = result.addWithOutOfBandPosition( paintTransform: MatrixUtils.forceToPoint(Offset.zero), // cannot be inverted hitTest: (BoxHitTestResult result) { fail('non-invertible transform should be caught'); }, ); }, throwsA(isAssertionError.having( (AssertionError error) => error.message, 'message', 'paintTransform must be invertible.', )), ); expect(isHit, isFalse); expect( () { isHit = result.addWithOutOfBandPosition( hitTest: (BoxHitTestResult result) { fail('addWithOutOfBandPosition should need some transformation of some sort'); }, ); }, throwsA(isAssertionError.having( (AssertionError error) => error.message, 'message', 'Exactly one transform or offset argument must be provided.', )), ); expect(isHit, isFalse); }); test('error message', () { { final RenderBox renderObject = RenderConstrainedBox( additionalConstraints: const BoxConstraints().tighten(height: 100.0), ); late FlutterError result; try { final BoxHitTestResult result = BoxHitTestResult(); renderObject.hitTest(result, position: Offset.zero); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' Cannot hit test a render box that has never been laid out.\n' ' The hitTest() method was called on this RenderBox: RenderConstrainedBox#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED:\n' ' parentData: MISSING\n' ' constraints: MISSING\n' ' size: MISSING\n' ' additionalConstraints: BoxConstraints(0.0<=w<=Infinity, h=100.0)\n' " Unfortunately, this object's geometry is not known at this time,\n" ' probably because it has never been laid out. This means it cannot\n' ' be accurately hit-tested.\n' ' If you are trying to perform a hit test during the layout phase\n' ' itself, make sure you only hit test nodes that have completed\n' " layout (e.g. the node's children, after their layout() method has\n" ' been called).\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'If you are trying to perform a hit test during the layout phase ' 'itself, make sure you only hit test nodes that have completed ' "layout (e.g. the node's children, after their layout() method has " 'been called).', ); } { late FlutterError result; final FakeMissingSizeRenderBox renderObject = FakeMissingSizeRenderBox(); layout(renderObject); renderObject.fakeMissingSize = true; try { final BoxHitTestResult result = BoxHitTestResult(); renderObject.hitTest(result, position: Offset.zero); } on FlutterError catch (e) { result = e; } expect(result, isNotNull); expect( result.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' Cannot hit test a render box with no size.\n' ' The hitTest() method was called on this RenderBox: FakeMissingSizeRenderBox#00000 NEEDS-PAINT:\n' ' parentData: <none>\n' ' constraints: BoxConstraints(w=800.0, h=600.0)\n' ' size: Size(800.0, 600.0)\n' ' Although this node is not marked as needing layout, its size is\n' ' not set.\n' ' A RenderBox object must have an explicit size before it can be\n' ' hit-tested. Make sure that the RenderBox in question sets its\n' ' size during layout.\n', ), ); expect( result.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(), 'A RenderBox object must have an explicit size before it can be ' 'hit-tested. Make sure that the RenderBox in question sets its ' 'size during layout.', ); } }); test('localToGlobal with ancestor', () { final RenderConstrainedBox innerConstrained = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 50, height: 50)); final RenderPositionedBox innerCenter = RenderPositionedBox(child: innerConstrained); final RenderConstrainedBox outerConstrained = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100, height: 100), child: innerCenter); final RenderPositionedBox outerCentered = RenderPositionedBox(child: outerConstrained); layout(outerCentered); expect(innerConstrained.localToGlobal(Offset.zero, ancestor: outerConstrained).dy, 25.0); }); }); test('Error message when size has not been set in RenderBox performLayout should be well versed', () { late FlutterErrorDetails errorDetails; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails details) { errorDetails = details; }; try { MissingSetSizeRenderBox().layout(const BoxConstraints()); } finally { FlutterError.onError = oldHandler; } expect(errorDetails, isNotNull); // Check the ErrorDetails without the stack trace. final List<String> lines = errorDetails.toString().split('\n'); expect( lines.take(5).join('\n'), equalsIgnoringHashCodes( '══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══════════════════════\n' 'The following assertion was thrown during performLayout():\n' 'RenderBox did not set its size during layout.\n' 'Because this RenderBox has sizedByParent set to false, it must\n' 'set its size in performLayout().', ), ); }); test('debugDoingBaseline flag is cleared after exception', () { final BadBaselineRenderBox badChild = BadBaselineRenderBox(); final RenderBox badRoot = RenderBaseline( child: badChild, baseline: 0.0, baselineType: TextBaseline.alphabetic, ); final List<dynamic> exceptions = <dynamic>[]; layout(badRoot, onErrors: () { exceptions.addAll(TestRenderingFlutterBinding.instance.takeAllFlutterExceptions()); }); expect(exceptions, isNotEmpty); final RenderBox goodRoot = RenderBaseline( child: RenderDecoratedBox(decoration: const BoxDecoration()), baseline: 0.0, baselineType: TextBaseline.alphabetic, ); layout(goodRoot, onErrors: () { assert(false); }); }); } class _DummyHitTestTarget implements HitTestTarget { @override void handleEvent(PointerEvent event, HitTestEntry entry) { // Nothing to do. } } class MyHitTestResult extends HitTestResult { void publicPushTransform(Matrix4 transform) => pushTransform(transform); }