Unverified Commit 3eb40925 authored by Dan Field's avatar Dan Field Committed by GitHub

Always test semantics (#35110)

Default `semanticsEnabled` to true for `testWidgets` and fix associated bugs
parent 97127730
...@@ -520,6 +520,7 @@ class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChil ...@@ -520,6 +520,7 @@ class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChil
void performLayout() { void performLayout() {
if (child == null || (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight)) { if (child == null || (!constraints.hasBoundedWidth && !constraints.hasBoundedHeight)) {
size = constraints.constrain(Size.zero); size = constraints.constrain(Size.zero);
child?.layout(SectorConstraints(maxDeltaRadius: innerRadius), parentUsesSize: true);
return; return;
} }
assert(child is RenderSector); assert(child is RenderSector);
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -66,14 +68,25 @@ class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate { ...@@ -66,14 +68,25 @@ class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
if (index == 0 || index == columnWidths.length - 1) if (index == 0 || index == columnWidths.length - 1)
childWidth += remainingWidth / 2; childWidth += remainingWidth / 2;
assert( // We can't actually assert here because it would break things badly for
childWidth >= 0, // semantics, which will expect that we laid things out here.
'Insufficient horizontal space to render the CupertinoDatePicker ' assert(() {
'because the parent is too narrow at ${size.width}px.\n' if (childWidth < 0) {
'An additional ${-remainingWidth}px is needed to avoid overlapping ' FlutterError.reportError(
'columns.', FlutterErrorDetails(
); exception: FlutterError(
layoutChild(index, BoxConstraints.tight(Size(childWidth, size.height))); 'Insufficient horizontal space to render the '
'CupertinoDatePicker because the parent is too narrow at '
'${size.width}px.\n'
'An additional ${-remainingWidth}px is needed to avoid '
'overlapping columns.',
),
),
);
}
return true;
}());
layoutChild(index, BoxConstraints.tight(Size(math.max(0.0, childWidth), size.height)));
positionChild(index, Offset(currentHorizontalOffset, 0.0)); positionChild(index, Offset(currentHorizontalOffset, 0.0));
currentHorizontalOffset += childWidth; currentHorizontalOffset += childWidth;
......
...@@ -3584,9 +3584,10 @@ class _SemanticsGeometry { ...@@ -3584,9 +3584,10 @@ class _SemanticsGeometry {
} else { } else {
_semanticsClipRect = _intersectRects(_semanticsClipRect, parent.describeApproximatePaintClip(child)); _semanticsClipRect = _intersectRects(_semanticsClipRect, parent.describeApproximatePaintClip(child));
} }
_semanticsClipRect = _transformRect(_semanticsClipRect, parent, child); _temporaryTransformHolder.setIdentity(); // clears data from previous call(s)
_paintClipRect = _transformRect(_paintClipRect, parent, child); _applyIntermediatePaintTransforms(parent, child, _transform, _temporaryTransformHolder);
parent.applyPaintTransform(child, _transform); _semanticsClipRect = _transformRect(_semanticsClipRect, _temporaryTransformHolder);
_paintClipRect = _transformRect(_paintClipRect, _temporaryTransformHolder);
} }
final RenderObject owner = ancestors.first; final RenderObject owner = ancestors.first;
...@@ -3609,14 +3610,41 @@ class _SemanticsGeometry { ...@@ -3609,14 +3610,41 @@ class _SemanticsGeometry {
static final Matrix4 _temporaryTransformHolder = Matrix4.zero(); static final Matrix4 _temporaryTransformHolder = Matrix4.zero();
/// From parent to child coordinate system. /// From parent to child coordinate system.
static Rect _transformRect(Rect rect, RenderObject parent, RenderObject child) { static Rect _transformRect(Rect rect, Matrix4 transform) {
assert(transform != null);
if (rect == null) if (rect == null)
return null; return null;
if (rect.isEmpty) if (rect.isEmpty)
return Rect.zero; return Rect.zero;
_temporaryTransformHolder.setIdentity(); // clears data from a previous call return MatrixUtils.inverseTransformRect(transform, rect);
parent.applyPaintTransform(child, _temporaryTransformHolder); }
return MatrixUtils.inverseTransformRect(_temporaryTransformHolder, rect);
// Calls applyPaintTransform on all of the render objects between [child] and
// [ancestor]. This method handles cases where the immediate semantic parent
// is not the immediate render object parent of the child.
//
// It will mutate both transform and clipRectTransform.
static void _applyIntermediatePaintTransforms(
RenderObject ancestor,
RenderObject child,
Matrix4 transform,
Matrix4 clipRectTransform,
) {
assert(ancestor != null);
assert(child != null);
assert(transform != null);
assert(clipRectTransform != null);
assert(clipRectTransform.isIdentity());
RenderObject intermediateParent = child.parent;
assert(intermediateParent != null);
while (intermediateParent != ancestor) {
intermediateParent.applyPaintTransform(child, transform);
intermediateParent = intermediateParent.parent;
child = child.parent;
assert(intermediateParent != null);
}
ancestor.applyPaintTransform(child, transform);
ancestor.applyPaintTransform(child, clipRectTransform);
} }
static Rect _intersectRects(Rect a, Rect b) { static Rect _intersectRects(Rect a, Rect b) {
......
...@@ -639,7 +639,7 @@ class RenderIndexedStack extends RenderStack { ...@@ -639,7 +639,7 @@ class RenderIndexedStack extends RenderStack {
@override @override
void visitChildrenForSemantics(RenderObjectVisitor visitor) { void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (index != null) if (index != null && firstChild != null)
visitor(_childAtIndex()); visitor(_childAtIndex());
} }
......
...@@ -815,7 +815,7 @@ void main() { ...@@ -815,7 +815,7 @@ void main() {
); );
final dynamic exception = tester.takeException(); final dynamic exception = tester.takeException();
expect(exception, isAssertionError); expect(exception, isFlutterError);
expect( expect(
exception.toString(), exception.toString(),
contains('Insufficient horizontal space to render the CupertinoDatePicker'), contains('Insufficient horizontal space to render the CupertinoDatePicker'),
......
...@@ -2280,6 +2280,7 @@ void main() { ...@@ -2280,6 +2280,7 @@ void main() {
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 16.0)); expect(tester.getSize(find.byType(InputDecorator)), const Size(800.0, 16.0));
expect(tester.getSize(find.text('text')).height, 16.0); expect(tester.getSize(find.text('text')).height, 16.0);
expect(tester.getTopLeft(find.text('text')).dy, 0.0); expect(tester.getTopLeft(find.text('text')).dy, 0.0);
......
...@@ -278,12 +278,15 @@ void main() { ...@@ -278,12 +278,15 @@ void main() {
double progressValue; double progressValue;
StateSetter setState; StateSetter setState;
await tester.pumpWidget( await tester.pumpWidget(
Center( Directionality(
child: StatefulBuilder( textDirection: TextDirection.ltr,
builder: (BuildContext context, StateSetter setter) { child: Center(
setState = setter; child: StatefulBuilder(
return CircularProgressIndicator(value: progressValue); builder: (BuildContext context, StateSetter setter) {
} setState = setter;
return CircularProgressIndicator(value: progressValue);
}
),
), ),
) )
); );
......
...@@ -1260,7 +1260,7 @@ void main() { ...@@ -1260,7 +1260,7 @@ void main() {
hasSemantics( hasSemantics(
TestSemantics.root(children: <TestSemantics>[ TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 2, id: 1,
value: '50%', value: '50%',
increasedValue: '60%', increasedValue: '60%',
decreasedValue: '40%', decreasedValue: '40%',
...@@ -1299,7 +1299,7 @@ void main() { ...@@ -1299,7 +1299,7 @@ void main() {
hasSemantics( hasSemantics(
TestSemantics.root(children: <TestSemantics>[ TestSemantics.root(children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 3, id: 1,
value: '40', value: '40',
increasedValue: '60', increasedValue: '60',
decreasedValue: '20', decreasedValue: '20',
......
...@@ -315,7 +315,7 @@ void _defineTests() { ...@@ -315,7 +315,7 @@ void _defineTests() {
expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull); expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);
semantics.dispose(); semantics.dispose();
}); }, semanticsEnabled: false);
testWidgets('Supports all actions', (WidgetTester tester) async { testWidgets('Supports all actions', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
......
...@@ -297,12 +297,15 @@ Future<void> main() async { ...@@ -297,12 +297,15 @@ Future<void> main() async {
final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage); final TestImageProvider placeholderProvider = TestImageProvider(placeholderImage);
final TestImageProvider imageProvider = TestImageProvider(targetImage); final TestImageProvider imageProvider = TestImageProvider(targetImage);
await tester.pumpWidget(FadeInImage( await tester.pumpWidget(Directionality(
placeholder: placeholderProvider, textDirection: TextDirection.ltr,
image: imageProvider, child: FadeInImage(
fadeOutDuration: animationDuration, placeholder: placeholderProvider,
fadeInDuration: animationDuration, image: imageProvider,
imageSemanticLabel: imageSemanticText, fadeOutDuration: animationDuration,
fadeInDuration: animationDuration,
imageSemanticLabel: imageSemanticText,
),
)); ));
placeholderProvider.complete(); placeholderProvider.complete();
......
...@@ -217,30 +217,30 @@ void main() { ...@@ -217,30 +217,30 @@ void main() {
); );
// SemanticsNode#0() // SemanticsNode#0()
// └SemanticsNode#8() // └SemanticsNode#1()
// ├SemanticsNode#5(selected, label: "node 1", textDirection: ltr) // ├SemanticsNode#2(selected, label: "node 1", textDirection: ltr)
// ├SemanticsNode#6(selected, label: "node 2", textDirection: ltr) // ├SemanticsNode#3(selected, label: "node 2", textDirection: ltr)
// └SemanticsNode#7(selected, label: "node 3", textDirection: ltr) // └SemanticsNode#4(selected, label: "node 3", textDirection: ltr)
expect( expect(
semantics, semantics,
hasSemantics( hasSemantics(
TestSemantics.root( TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 5, id: 1,
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 6, id: 2,
flags: SemanticsFlag.isSelected.index, flags: SemanticsFlag.isSelected.index,
label: 'node 1', label: 'node 1',
), ),
TestSemantics( TestSemantics(
id: 7, id: 3,
flags: SemanticsFlag.isSelected.index, flags: SemanticsFlag.isSelected.index,
label: 'node 2', label: 'node 2',
), ),
TestSemantics( TestSemantics(
id: 8, id: 4,
flags: SemanticsFlag.isSelected.index, flags: SemanticsFlag.isSelected.index,
label: 'node 3', label: 'node 3',
), ),
......
...@@ -238,6 +238,7 @@ void main() { ...@@ -238,6 +238,7 @@ void main() {
' │ needs compositing\n' ' │ needs compositing\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantics node: SemanticsNode#1\n'
' │ semantic boundary\n' ' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │\n' ' │\n'
...@@ -384,6 +385,7 @@ void main() { ...@@ -384,6 +385,7 @@ void main() {
' │ needs compositing\n' ' │ needs compositing\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantics node: SemanticsNode#1\n'
' │ semantic boundary\n' ' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │\n' ' │\n'
......
...@@ -406,7 +406,7 @@ void main() { ...@@ -406,7 +406,7 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreId: true, ignoreRect: true, ignoreTransform: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreId: true, ignoreRect: true, ignoreTransform: true));
semantics.dispose(); semantics.dispose();
}); }, semanticsEnabled: false);
group('showOnScreen', () { group('showOnScreen', () {
......
...@@ -62,7 +62,7 @@ void main() { ...@@ -62,7 +62,7 @@ void main() {
ignoreId: true, ignoreId: true,
)); ));
semantics.dispose(); semantics.dispose();
}); }, semanticsEnabled: false);
testWidgets('Detach and reattach assert', (WidgetTester tester) async { testWidgets('Detach and reattach assert', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
......
...@@ -55,16 +55,16 @@ void main() { ...@@ -55,16 +55,16 @@ void main() {
expect(semantics, hasSemantics(TestSemantics.root( expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics.rootChild( TestSemantics.rootChild(
id: 2, id: 1,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 3, id: 2,
rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0), rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0),
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[ children: <TestSemantics>[
TestSemantics( TestSemantics(
id: 4, id: 3,
label: 'Hello!', label: 'Hello!',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
......
...@@ -91,6 +91,7 @@ void main() { ...@@ -91,6 +91,7 @@ void main() {
' │ └─child: RenderParagraph#00000\n' ' │ └─child: RenderParagraph#00000\n'
' │ │ parentData: <none> (can use size)\n' ' │ │ parentData: <none> (can use size)\n'
' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ │ semantics node: SemanticsNode#2\n'
' │ │ size: Size(800.0, 600.0)\n' ' │ │ size: Size(800.0, 600.0)\n'
' │ │ textAlign: start\n' ' │ │ textAlign: start\n'
' │ │ textDirection: ltr\n' ' │ │ textDirection: ltr\n'
...@@ -115,6 +116,7 @@ void main() { ...@@ -115,6 +116,7 @@ void main() {
' └─child: RenderParagraph#00000\n' ' └─child: RenderParagraph#00000\n'
' │ parentData: <none> (can use size)\n' ' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantics node: SemanticsNode#3\n'
' │ size: Size(800.0, 600.0)\n' ' │ size: Size(800.0, 600.0)\n'
' │ textAlign: start\n' ' │ textAlign: start\n'
' │ textDirection: ltr\n' ' │ textDirection: ltr\n'
...@@ -126,6 +128,7 @@ void main() { ...@@ -126,6 +128,7 @@ void main() {
' ║ <all styles inherited>\n' ' ║ <all styles inherited>\n'
' ║ "1"\n' ' ║ "1"\n'
' ╚═══════════\n' ' ╚═══════════\n'
''
), ),
); );
}); });
......
...@@ -103,7 +103,7 @@ void main() { ...@@ -103,7 +103,7 @@ void main() {
expect(response['isError'], true); expect(response['isError'], true);
expect(response['response'], contains('Bad state: No semantics data found')); expect(response['response'], contains('Bad state: No semantics data found'));
}); }, semanticsEnabled: false);
testWidgets('throws state error multiple matches are found', (WidgetTester tester) async { testWidgets('throws state error multiple matches are found', (WidgetTester tester) async {
final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
......
...@@ -71,7 +71,7 @@ typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester); ...@@ -71,7 +71,7 @@ typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester);
/// If the `semanticsEnabled` parameter is set to `true`, /// If the `semanticsEnabled` parameter is set to `true`,
/// [WidgetTester.ensureSemantics] will have been called before the tester is /// [WidgetTester.ensureSemantics] will have been called before the tester is
/// passed to the `callback`, and that handle will automatically be disposed /// passed to the `callback`, and that handle will automatically be disposed
/// after the callback is finished. /// after the callback is finished. It defaults to true.
/// ///
/// This function uses the [test] function in the test package to /// This function uses the [test] function in the test package to
/// register the given callback as a test. The callback, when run, /// register the given callback as a test. The callback, when run,
...@@ -100,7 +100,7 @@ void testWidgets( ...@@ -100,7 +100,7 @@ void testWidgets(
bool skip = false, bool skip = false,
test_package.Timeout timeout, test_package.Timeout timeout,
Duration initialTimeout, Duration initialTimeout,
bool semanticsEnabled = false, bool semanticsEnabled = true,
}) { }) {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
final WidgetTester tester = WidgetTester._(binding); final WidgetTester tester = WidgetTester._(binding);
...@@ -115,6 +115,7 @@ void testWidgets( ...@@ -115,6 +115,7 @@ void testWidgets(
test_package.addTearDown(binding.postTest); test_package.addTearDown(binding.postTest);
return binding.runTest( return binding.runTest(
() async { () async {
debugResetSemanticsIdCounter();
await callback(tester); await callback(tester);
semanticsHandle?.dispose(); semanticsHandle?.dispose();
}, },
......
...@@ -123,7 +123,7 @@ void main() { ...@@ -123,7 +123,7 @@ void main() {
final Evaluation result = await textContrastGuideline.evaluate(tester); final Evaluation result = await textContrastGuideline.evaluate(tester);
expect(result.passed, false); expect(result.passed, false);
expect(result.reason, expect(result.reason,
'SemanticsNode#21(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",' 'SemanticsNode#3(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",'
' textDirection: ltr):\nExpected contrast ratio of at least ' ' textDirection: ltr):\nExpected contrast ratio of at least '
'4.5 but found 1.17 for a font size of 14.0. The ' '4.5 but found 1.17 for a font size of 14.0. The '
'computed light color was: Color(0xfffafafa), The computed dark color was:' 'computed light color was: Color(0xfffafafa), The computed dark color was:'
...@@ -256,7 +256,7 @@ void main() { ...@@ -256,7 +256,7 @@ void main() {
final Evaluation result = await androidTapTargetGuideline.evaluate(tester); final Evaluation result = await androidTapTargetGuideline.evaluate(tester);
expect(result.passed, false); expect(result.passed, false);
expect(result.reason, expect(result.reason,
'SemanticsNode#41(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap ' 'SemanticsNode#3(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap '
'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)\n' 'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)\n'
'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en'); 'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en');
handle.dispose(); handle.dispose();
......
...@@ -33,7 +33,7 @@ void main() { ...@@ -33,7 +33,7 @@ void main() {
group('semantics', () { group('semantics', () {
testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async { testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async {
expect(() => find.bySemanticsLabel('Add'), throwsStateError); expect(() => find.bySemanticsLabel('Add'), throwsStateError);
}); }, semanticsEnabled: false);
testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async { testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async {
final SemanticsHandle semanticsHandle = tester.ensureSemantics(); final SemanticsHandle semanticsHandle = tester.ensureSemantics();
......
...@@ -542,7 +542,7 @@ void main() { ...@@ -542,7 +542,7 @@ void main() {
expect(() => tester.getSemantics(find.text('hello')), expect(() => tester.getSemantics(find.text('hello')),
throwsA(isInstanceOf<StateError>())); throwsA(isInstanceOf<StateError>()));
}); }, semanticsEnabled: false);
testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async { testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async {
final SemanticsHandle semanticsHandle = tester.ensureSemantics(); final SemanticsHandle semanticsHandle = tester.ensureSemantics();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment