1137 lines
41 KiB
Dart
1137 lines
41 KiB
Dart
// 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() { }
|
|
}
|
|
|
|
void main() {
|
|
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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
constrainedAxis: Axis.horizontal, // This is reset to null below.
|
|
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.constrainedAxis = null;
|
|
renderer.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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
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 = renderer.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', () {
|
|
final RenderConstrainedBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: double.maxFinite));
|
|
final RenderConstraintsTransformBox box = RenderConstraintsTransformBox(
|
|
alignment: Alignment.center,
|
|
textDirection: TextDirection.ltr,
|
|
constraintsTransform: (BoxConstraints constraints) => constraints.copyWith(maxWidth: double.infinity),
|
|
child: child,
|
|
);
|
|
|
|
layout(box, constraints: const BoxConstraints(), phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
|
|
});
|
|
|
|
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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
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(
|
|
'RenderUnconstrainedBox#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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
constrainedAxis: Axis.horizontal,
|
|
textDirection: TextDirection.ltr,
|
|
child: RenderFlex(
|
|
direction: Axis.horizontal,
|
|
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 RenderUnconstrainedBox unconstrained = RenderUnconstrainedBox(
|
|
constrainedAxis: Axis.vertical,
|
|
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();
|
|
|
|
// By default, clipBehavior should be Clip.none
|
|
final RenderUnconstrainedBox defaultBox = RenderUnconstrainedBox(
|
|
alignment: Alignment.center,
|
|
textDirection: TextDirection.ltr,
|
|
child: box200x200,
|
|
);
|
|
layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
|
|
defaultBox.paint(context, Offset.zero);
|
|
expect(context.clipBehavior, equals(Clip.none));
|
|
|
|
for (final Clip clip in Clip.values) {
|
|
final RenderUnconstrainedBox box = RenderUnconstrainedBox(
|
|
alignment: Alignment.center,
|
|
textDirection: TextDirection.ltr,
|
|
child: box200x200,
|
|
clipBehavior: clip,
|
|
);
|
|
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
|
|
box.paint(context, Offset.zero);
|
|
expect(context.clipBehavior, equals(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(alignment: Alignment.center, child: innerConstrained);
|
|
final RenderConstrainedBox outerConstrained = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 100, height: 100), child: innerCenter);
|
|
final RenderPositionedBox outerCentered = RenderPositionedBox(alignment: Alignment.center, 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().',
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
class _DummyHitTestTarget implements HitTestTarget {
|
|
@override
|
|
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
|
// Nothing to do.
|
|
}
|
|
}
|
|
|
|
class MyHitTestResult extends HitTestResult {
|
|
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
|
|
}
|