// 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/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'rendering_tester.dart'; class RenderTestBox extends RenderBox { late Size boxSize; int calls = 0; double value = 0.0; double next() { value += 1.0; return value; } @override double computeMinIntrinsicWidth(double height) => next(); @override double computeMaxIntrinsicWidth(double height) => next(); @override double computeMinIntrinsicHeight(double width) => next(); @override double computeMaxIntrinsicHeight(double width) => next(); @override void performLayout() { size = constraints.biggest; boxSize = size; } @override double? computeDistanceToActualBaseline(TextBaseline baseline) { if (!RenderObject.debugCheckingIntrinsics) { calls += 1; } return boxSize.height / 2.0; } } class RenderDryBaselineTestBox extends RenderTestBox { double? baselineOverride; @override double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { if (!RenderObject.debugCheckingIntrinsics) { calls += 1; } return baselineOverride ?? constraints.biggest.height / 2.0; } } class RenderBadDryBaselineTestBox extends RenderTestBox { @override double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { return size.height / 2.0; } } class RenderCannotComputeDryBaselineTestBox extends RenderTestBox { bool shouldAssert = true; @override double? computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) { if (shouldAssert) { assert(debugCannotComputeDryLayout(reason: 'no dry baseline for you')); } return null; } } void main() { TestRenderingFlutterBinding.ensureInitialized(); test('Intrinsics cache', () { final RenderBox test = RenderTestBox(); expect(test.getMinIntrinsicWidth(0.0), equals(1.0)); expect(test.getMinIntrinsicWidth(100.0), equals(2.0)); expect(test.getMinIntrinsicWidth(200.0), equals(3.0)); expect(test.getMinIntrinsicWidth(0.0), equals(1.0)); expect(test.getMinIntrinsicWidth(100.0), equals(2.0)); expect(test.getMinIntrinsicWidth(200.0), equals(3.0)); expect(test.getMaxIntrinsicWidth(0.0), equals(4.0)); expect(test.getMaxIntrinsicWidth(100.0), equals(5.0)); expect(test.getMaxIntrinsicWidth(200.0), equals(6.0)); expect(test.getMaxIntrinsicWidth(0.0), equals(4.0)); expect(test.getMaxIntrinsicWidth(100.0), equals(5.0)); expect(test.getMaxIntrinsicWidth(200.0), equals(6.0)); expect(test.getMinIntrinsicHeight(0.0), equals(7.0)); expect(test.getMinIntrinsicHeight(100.0), equals(8.0)); expect(test.getMinIntrinsicHeight(200.0), equals(9.0)); expect(test.getMinIntrinsicHeight(0.0), equals(7.0)); expect(test.getMinIntrinsicHeight(100.0), equals(8.0)); expect(test.getMinIntrinsicHeight(200.0), equals(9.0)); expect(test.getMaxIntrinsicHeight(0.0), equals(10.0)); expect(test.getMaxIntrinsicHeight(100.0), equals(11.0)); expect(test.getMaxIntrinsicHeight(200.0), equals(12.0)); expect(test.getMaxIntrinsicHeight(0.0), equals(10.0)); expect(test.getMaxIntrinsicHeight(100.0), equals(11.0)); expect(test.getMaxIntrinsicHeight(200.0), equals(12.0)); // now read them all again backwards expect(test.getMaxIntrinsicHeight(200.0), equals(12.0)); expect(test.getMaxIntrinsicHeight(100.0), equals(11.0)); expect(test.getMaxIntrinsicHeight(0.0), equals(10.0)); expect(test.getMinIntrinsicHeight(200.0), equals(9.0)); expect(test.getMinIntrinsicHeight(100.0), equals(8.0)); expect(test.getMinIntrinsicHeight(0.0), equals(7.0)); expect(test.getMaxIntrinsicWidth(200.0), equals(6.0)); expect(test.getMaxIntrinsicWidth(100.0), equals(5.0)); expect(test.getMaxIntrinsicWidth(0.0), equals(4.0)); expect(test.getMinIntrinsicWidth(200.0), equals(3.0)); expect(test.getMinIntrinsicWidth(100.0), equals(2.0)); expect(test.getMinIntrinsicWidth(0.0), equals(1.0)); }); // Regression test for https://github.com/flutter/flutter/issues/101179 test('Cached baselines should be cleared if its parent re-layout', () { var viewHeight = 200.0; final test = RenderTestBox(); final RenderBox baseline = RenderBaseline( baseline: 0.0, baselineType: TextBaseline.alphabetic, child: test, ); final root = RenderConstrainedBox( additionalConstraints: BoxConstraints.tightFor(width: 200.0, height: viewHeight), child: baseline, ); layout(RenderPositionedBox(child: root)); var parentData = test.parentData as BoxParentData?; expect(parentData!.offset.dy, -(viewHeight / 2.0)); expect(test.calls, 1); // Trigger the root render re-layout. viewHeight = 300.0; root.additionalConstraints = BoxConstraints.tightFor(width: 200.0, height: viewHeight); pumpFrame(); parentData = test.parentData as BoxParentData?; expect(parentData!.offset.dy, -(viewHeight / 2.0)); expect(test.calls, 2); // The layout constraints change will clear the cached data. final RenderObject parent = test.parent!; expect(parent.debugNeedsLayout, false); // Do not forget notify parent dirty after the cached data be cleared by `layout()` test.markNeedsLayout(); expect(parent.debugNeedsLayout, true); pumpFrame(); expect(parent.debugNeedsLayout, false); expect(test.calls, 3); // Self dirty will clear the cached data. parent.markNeedsLayout(); pumpFrame(); expect(test.calls, 3); // Use the cached data if the layout constraints do not change. }); group('Dry baseline', () { test( 'computeDryBaseline results are cached and shared with computeDistanceToActualBaseline', () { const viewHeight = 200.0; const constraints = BoxConstraints.tightFor(width: 200.0, height: viewHeight); final test = RenderDryBaselineTestBox(); final RenderBox baseline = RenderBaseline( baseline: 0.0, baselineType: TextBaseline.alphabetic, child: test, ); final root = RenderConstrainedBox(additionalConstraints: constraints, child: baseline); layout(RenderPositionedBox(child: root)); expect(test.calls, 1); // The baseline widget loosens the input constraints when passing on to child. expect( test.getDryBaseline(constraints.loosen(), TextBaseline.alphabetic), test.boxSize.height / 2, ); // There's cache for the constraints so this should be 1, but we always evaluate // computeDryBaseline in debug mode in case it asserts even if the baseline // cache hits. expect(test.calls, 2); const newConstraints = BoxConstraints.tightFor(width: 10.0, height: 10.0); expect(test.getDryBaseline(newConstraints.loosen(), TextBaseline.alphabetic), 5.0); // Should be 3 but there's an additional computeDryBaseline call in getDryBaseline, // in an assert. expect(test.calls, 4); root.additionalConstraints = newConstraints; pumpFrame(); expect(test.calls, 4); }, ); test('Asserts when a RenderBox cannot compute dry baseline', () { final test = RenderCannotComputeDryBaselineTestBox(); layout(RenderBaseline(baseline: 0.0, baselineType: TextBaseline.alphabetic, child: test)); final BoxConstraints incomingConstraints = test.constraints; assert(incomingConstraints != const BoxConstraints()); expect( () => test.getDryBaseline(const BoxConstraints(), TextBaseline.alphabetic), throwsA( isA().having( (AssertionError e) => e.message, 'message', contains('no dry baseline for you'), ), ), ); // Still throws when there is cache. expect( () => test.getDryBaseline(incomingConstraints, TextBaseline.alphabetic), throwsA( isA().having( (AssertionError e) => e.message, 'message', contains('no dry baseline for you'), ), ), ); }); test( 'Catches inconsistencies between computeDryBaseline and computeDistanceToActualBaseline', () { final test = RenderDryBaselineTestBox(); layout(test, phase: EnginePhase.composite); FlutterErrorDetails? error; test.markNeedsLayout(); test.baselineOverride = 123; pumpFrame( phase: EnginePhase.composite, onErrors: () { error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails(); }, ); expect( error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'), ); }, ); test('Accessing RenderBox.size in computeDryBaseline is not allowed', () { final test = RenderBadDryBaselineTestBox(); FlutterErrorDetails? error; layout( test, phase: EnginePhase.composite, onErrors: () { error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails(); }, ); expect( error?.exceptionAsString(), contains('RenderBox.size accessed in RenderBadDryBaselineTestBox.computeDryBaseline.'), ); }); test('debug baseline checks do not freak out when debugCannotComputeDryLayout is called', () { FlutterErrorDetails? error; void onErrors() { error = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails(); } final test = RenderCannotComputeDryBaselineTestBox(); layout(test, phase: EnginePhase.composite, onErrors: onErrors); expect(error, isNull); test.shouldAssert = false; test.markNeedsLayout(); pumpFrame(phase: EnginePhase.composite, onErrors: onErrors); expect( error?.exceptionAsString(), contains('differs from the baseline location computed by computeDryBaseline'), ); }); }); }