| // 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, |
| ), |
| ), |
| ); |
| break; |
| 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, |
| ), |
| ), |
| ); |
| break; |
| } |
| 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'); |
| break; |
| case Clip.hardEdge: |
| case Clip.antiAlias: |
| case Clip.antiAliasWithSaveLayer: |
| expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip'); |
| break; |
| } |
| 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!, |
| ); |
| break; |
| case null: |
| box = RenderConstraintsTransformBox( |
| constraintsTransform: ConstraintsTransformBox.unconstrained, |
| alignment: Alignment.center, |
| textDirection: TextDirection.ltr, |
| child: box200x200, |
| ); |
| break; |
| } |
| 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'); |
| break; |
| case Clip.hardEdge: |
| case Clip.antiAlias: |
| case Clip.antiAliasWithSaveLayer: |
| expect(hadErrors, isFalse, reason: 'Should not have had overflow errors for $clip'); |
| break; |
| } |
| 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); |
| } |