Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

1377 lines
48 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() {}
}
class BadBaselineRenderBox extends RenderBox {
@override
void performLayout() {
size = constraints.biggest;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
throw Exception();
}
}
class InvalidSizeAccessInDryLayoutBox extends RenderBox {
@override
Size computeDryLayout(covariant BoxConstraints constraints) {
return constraints.constrain(hasSize ? size : Size.infinite);
}
@override
void performLayout() {
size = getDryLayout(constraints);
}
}
class _BaselineSizeAccessRootRenderBox extends RenderProxyBox {
_BaselineSizeAccessRootRenderBox({required RenderBox child}) : super(child);
@override
void performLayout() {
super.performLayout();
child!.getDistanceToBaseline(TextBaseline.alphabetic);
}
}
class _BaselineSizeAccessChildRenderBox extends RenderProxyBox {
_BaselineSizeAccessChildRenderBox({required RenderBox child}) : super(child);
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return child!.size.height;
}
}
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 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(wrapWidth: 640),
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: ${const Color(0xff00ff00)}\n'
' configuration: ImageConfiguration()\n'
' However, this second render object is not, or is no longer, a '
'child of the first, and it is therefore a violation of the '
'RenderBox layout protocol to use that size in the layout of the '
'first render object.\n'
' If the size was obtained at a time where it was valid to read '
'the size (because the second render object above was a child of '
'the first at the time), then it should be adopted using '
'debugAdoptSize at that time.\n'
' If the size comes from a grandchild or a render object from an '
'entirely different part of the render tree, then there is no way '
'to be notified when the size changes and therefore attempts to '
'read that size are almost certainly a source of bugs. A different '
'approach should be used.\n',
),
);
expect(
result.diagnostics
.where((DiagnosticsNode node) => node.level == DiagnosticLevel.hint)
.length,
2,
);
}
});
test('Invalid size access error message', () {
final testBox = InvalidSizeAccessInDryLayoutBox();
late FlutterErrorDetails errorDetails;
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
errorDetails = details;
};
try {
testBox.layout(const BoxConstraints.tightFor(width: 100.0, height: 100.0));
} finally {
FlutterError.onError = oldHandler;
}
expect(
errorDetails.toString().replaceAll('\n', ' '),
contains(
'RenderBox.size accessed in '
'InvalidSizeAccessInDryLayoutBox.computeDryLayout. '
"The computeDryLayout method must not access the RenderBox's own size, or the size of its child, "
"because it's established in performLayout or performResize using different BoxConstraints.",
),
);
});
// Regression test for https://github.com/flutter/flutter/issues/157915.
test('Does not throw when accessing child size from computeDistanceToActualBaseline', () {
final RenderBox leaf = RenderPadding(padding: const EdgeInsets.all(10.0));
final child = _BaselineSizeAccessChildRenderBox(child: leaf);
final root = _BaselineSizeAccessRootRenderBox(child: child);
var hadErrors = false;
void expectSizeAccessErrors() {
absorbOverflowedErrors();
hadErrors = true;
}
layout(root, onErrors: expectSizeAccessErrors);
expect(hadErrors, false);
});
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 coloredBox = RenderDecoratedBox(decoration: const BoxDecoration());
final paddedBox = RenderPadding(child: coloredBox, padding: const EdgeInsets.all(10.0));
layout(paddedBox);
final parentData = coloredBox.parentData! as BoxParentData;
expect(parentData.offset.dx, isNot(equals(0.0)));
paddedBox.child = null;
final constrainedBox = RenderConstrainedBox(
child: coloredBox,
additionalConstraints: const BoxConstraints(),
);
layout(constrainedBox);
expect(coloredBox.parentData?.runtimeType, ParentData);
});
test('UnconstrainedBox expands to fit children', () {
final 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 unconstrained = RenderConstraintsTransformBox(
constraintsTransform: ConstraintsTransformBox.unconstrained,
textDirection: TextDirection.ltr,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 200.0),
),
alignment: Alignment.center,
);
const 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 unconstrained = RenderConstraintsTransformBox(
constraintsTransform: ConstraintsTransformBox.unconstrained,
textDirection: TextDirection.ltr,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 200.0),
),
alignment: Alignment.center,
);
const 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 child = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 0),
);
final 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', () {
var hadErrors = false;
void expectOverflowedErrors() {
absorbOverflowedErrors();
hadErrors = true;
}
final context = TestClipPaintingContext();
for (final 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 child = RenderParagraph(TextSpan(text: 'a' * 100), textDirection: TextDirection.ltr);
final 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('paints even when its size is empty', () {
// Regression test for https://github.com/flutter/flutter/issues/146840.
final child = RenderParagraph(const TextSpan(text: ''), textDirection: TextDirection.ltr);
final box = RenderConstraintsTransformBox(
alignment: Alignment.center,
textDirection: TextDirection.ltr,
constraintsTransform: (BoxConstraints constraints) =>
constraints.copyWith(maxWidth: double.infinity),
child: child,
);
layout(box, constraints: BoxConstraints.tight(Size.zero), phase: EnginePhase.paint);
expect(box, paints..paragraph());
});
});
test('getMinIntrinsicWidth error handling', () {
final unconstrained = RenderConstraintsTransformBox(
constraintsTransform: ConstraintsTransformBox.unconstrained,
textDirection: TextDirection.ltr,
child: RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 200.0),
),
alignment: Alignment.center,
);
const 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 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 flexible = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.expand(height: 200.0),
);
final unconstrained = RenderConstraintsTransformBox(
constraintsTransform: ConstraintsTransformBox.heightUnconstrained,
textDirection: TextDirection.ltr,
child: RenderFlex(textDirection: TextDirection.ltr, children: <RenderBox>[flexible]),
alignment: Alignment.center,
);
final flexParentData = flexible.parentData! as FlexParentData;
flexParentData.flex = 1;
flexParentData.fit = FlexFit.tight;
const 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 flexible = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.expand(width: 200.0),
);
final unconstrained = RenderConstraintsTransformBox(
constraintsTransform: ConstraintsTransformBox.widthUnconstrained,
textDirection: TextDirection.ltr,
child: RenderFlex(
direction: Axis.vertical,
textDirection: TextDirection.ltr,
children: <RenderBox>[flexible],
),
alignment: Alignment.center,
);
final flexParentData = flexible.parentData! as FlexParentData;
flexParentData.flex = 1;
flexParentData.fit = FlexFit.tight;
const 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 viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
final context = TestClipPaintingContext();
var hadErrors = false;
void expectOverflowedErrors() {
absorbOverflowedErrors();
hadErrors = true;
}
for (final 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 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 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 result = BoxHitTestResult();
final 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 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 result = BoxHitTestResult();
final 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 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 result = BoxHitTestResult();
final 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 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 result = BoxHitTestResult();
var 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 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 renderObject = FakeMissingSizeRenderBox();
layout(renderObject);
renderObject.fakeMissingSize = true;
try {
final 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 innerConstrained = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 50, height: 50),
);
final innerCenter = RenderPositionedBox(child: innerConstrained);
final outerConstrained = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(width: 100, height: 100),
child: innerCenter,
);
final 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 badChild = BadBaselineRenderBox();
final RenderBox badRoot = RenderBaseline(
child: badChild,
baseline: 0.0,
baselineType: TextBaseline.alphabetic,
);
final 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);
},
);
});
group('BaselineOffset', () {
test('minOf', () {
expect(BaselineOffset.noBaseline.minOf(BaselineOffset.noBaseline), BaselineOffset.noBaseline);
expect(BaselineOffset.noBaseline.minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(BaselineOffset.noBaseline), const BaselineOffset(1));
expect(const BaselineOffset(2).minOf(const BaselineOffset(1)), const BaselineOffset(1));
expect(const BaselineOffset(1).minOf(const BaselineOffset(2)), const BaselineOffset(1));
});
test('+', () {
expect(BaselineOffset.noBaseline + 2, BaselineOffset.noBaseline);
expect(const BaselineOffset(1) + 2, const BaselineOffset(3));
});
});
}
class _DummyHitTestTarget implements HitTestTarget {
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
// Nothing to do.
}
}
class MyHitTestResult extends HitTestResult {
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
}