diff --git a/packages/flutter/lib/src/material/drawer.dart b/packages/flutter/lib/src/material/drawer.dart index dde0a504e1..b9f32ec699 100644 --- a/packages/flutter/lib/src/material/drawer.dart +++ b/packages/flutter/lib/src/material/drawer.dart @@ -270,10 +270,12 @@ class DrawerControllerState extends State with SingleTickerPro child: new RepaintBoundary( child: new Stack( children: [ - new GestureDetector( - onTap: close, - child: new Container( - color: _color.evaluate(_controller) + new BlockSemantics( + child: new GestureDetector( + onTap: close, + child: new Container( + color: _color.evaluate(_controller) + ), ), ), new Align( diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 4e4bae3fad..1e2a4771c3 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -662,7 +662,8 @@ abstract class _SemanticsFragment { _SemanticsFragment({ @required RenderObject renderObjectOwner, this.annotator, - List<_SemanticsFragment> children + List<_SemanticsFragment> children, + this.dropSemanticsOfPreviousSiblings, }) { assert(renderObjectOwner != null); _ancestorChain = [renderObjectOwner]; @@ -678,6 +679,9 @@ abstract class _SemanticsFragment { } final SemanticsAnnotator annotator; + bool dropSemanticsOfPreviousSiblings; + + bool get producesSemanticNodes => true; List _ancestorChain; void addAncestor(RenderObject ancestor) { @@ -695,6 +699,20 @@ abstract class _SemanticsFragment { String toString() => '$runtimeType#$hashCode'; } +/// A SemanticsFragment that doesn't produce any [SemanticsNode]s when compiled. +class _EmptySemanticsFragment extends _SemanticsFragment { + _EmptySemanticsFragment({ + @required RenderObject renderObjectOwner, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); + + @override + Iterable compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* { } + + @override + bool get producesSemanticNodes => false; +} + /// Represents a [RenderObject] which is in no way dirty. /// /// This class has no children and no annotators, and when compiled, it returns @@ -702,8 +720,9 @@ abstract class _SemanticsFragment { /// the matrix, since that comes from the (dirty) ancestors.) class _CleanSemanticsFragment extends _SemanticsFragment { _CleanSemanticsFragment({ - @required RenderObject renderObjectOwner - }) : super(renderObjectOwner: renderObjectOwner) { + @required RenderObject renderObjectOwner, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings) { assert(renderObjectOwner != null); assert(renderObjectOwner._semantics != null); } @@ -728,8 +747,9 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { _InterestingSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); bool get haveConcreteNode => true; @@ -765,8 +785,9 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { _RootSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { @@ -798,8 +819,9 @@ class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment { _ConcreteSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) { @@ -833,8 +855,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { _ImplicitSemanticsFragment({ RenderObject renderObjectOwner, SemanticsAnnotator annotator, - Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children); + Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); @override bool get haveConcreteNode => _haveConcreteNode; @@ -878,8 +901,9 @@ class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment { class _ForkingSemanticsFragment extends _SemanticsFragment { _ForkingSemanticsFragment({ RenderObject renderObjectOwner, - @required Iterable<_SemanticsFragment> children - }) : super(renderObjectOwner: renderObjectOwner, children: children) { + @required Iterable<_SemanticsFragment> children, + bool dropSemanticsOfPreviousSiblings, + }) : super(renderObjectOwner: renderObjectOwner, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings) { assert(children != null); assert(children.length > 1); } @@ -1414,6 +1438,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { super.adoptChild(child); markNeedsLayout(); markNeedsCompositingBitsUpdate(); + markNeedsSemanticsUpdate(); } /// Called by subclasses when they decide a render object is no longer a child. @@ -1431,6 +1456,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { super.dropChild(child); markNeedsLayout(); markNeedsCompositingBitsUpdate(); + markNeedsSemanticsUpdate(); } /// Calls visitor for each immediate child of this render object. @@ -2408,6 +2434,22 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// setting [isSemanticBoundary] to true. bool get isSemanticBoundary => false; + /// Whether this [RenderObject] makes other [RenderObject]s previously painted + /// within the same semantic boundary unreachable for accessibility purposes. + /// + /// If `true` is returned, the [SemanticsNode]s for all siblings and cousins + /// of this node, that are earlier in a depth-first pre-order traversal, are + /// dropped from the semantics tree up until a semantic boundary (as defined + /// by [isSemanticBoundary]) is reached. + /// + /// If [isSemanticBoundary] and [isBlockingSemanticsOfPreviouslyPaintedNodes] + /// is set on the same node, all previously painted siblings and cousins + /// up until the next ancestor that is a semantic boundary are dropped. + /// + /// Paint order as established by [visitChildrenForSemantics] is used to + /// determine if a node is previous to this one. + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => false; + /// The bounding box, in the local coordinate system, of this /// object, for accessibility purposes. Rect get semanticBounds; @@ -2545,9 +2587,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { // early-exit if we're not dirty and have our own semantics if (!_needsSemanticsUpdate && isSemanticBoundary) { assert(_semantics != null); - return new _CleanSemanticsFragment(renderObjectOwner: this); + return new _CleanSemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: isBlockingSemanticsOfPreviouslyPaintedNodes); } List<_SemanticsFragment> children; + bool dropSemanticsOfPreviousSiblings = isBlockingSemanticsOfPreviouslyPaintedNodes; visitChildrenForSemantics((RenderObject child) { if (_needsSemanticsGeometryUpdate) { // If our geometry changed, make sure the child also does a @@ -2557,31 +2600,40 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { child._needsSemanticsGeometryUpdate = true; } final _SemanticsFragment fragment = child._getSemanticsFragment(); - if (fragment != null) { + assert(fragment != null); + if (fragment.dropSemanticsOfPreviousSiblings) { + children = null; // throw away all left siblings of [child]. + dropSemanticsOfPreviousSiblings = true; + } + if (fragment.producesSemanticNodes) { fragment.addAncestor(this); children ??= <_SemanticsFragment>[]; assert(!children.contains(fragment)); children.add(fragment); } }); + if (isSemanticBoundary && !isBlockingSemanticsOfPreviouslyPaintedNodes) { + // Don't propagate [dropSemanticsOfPreviousSiblings] up through a semantic boundary. + dropSemanticsOfPreviousSiblings = false; + } _needsSemanticsUpdate = false; _needsSemanticsGeometryUpdate = false; final SemanticsAnnotator annotator = semanticsAnnotator; if (parent is! RenderObject) - return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _RootSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); if (isSemanticBoundary) - return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _ConcreteSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); if (annotator != null) - return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children); + return new _ImplicitSemanticsFragment(renderObjectOwner: this, annotator: annotator, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); _semantics = null; if (children == null) { // Introduces no semantics and has no descendants that introduce semantics. - return null; + return new _EmptySemanticsFragment(renderObjectOwner: this, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); } if (children.length > 1) - return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children); + return new _ForkingSemanticsFragment(renderObjectOwner: this, children: children, dropSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings); assert(children.length == 1); - return children.single; + return children.single..dropSemanticsOfPreviousSiblings = dropSemanticsOfPreviousSiblings; } /// Called when collecting the semantics of this node. @@ -2727,6 +2779,10 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { description.add('layer: $_layer'); if (_semantics != null) description.add('semantics: $_semantics'); + if (isBlockingSemanticsOfPreviouslyPaintedNodes) + description.add('blocks semantics of earlier render objects below the common boundary'); + if (isSemanticBoundary) + description.add('semantic boundary'); } /// Returns a string describing the current node's descendants. Each line of diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 1dfd41691b..1fadc45316 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2891,6 +2891,18 @@ class RenderSemanticsAnnotations extends RenderProxyBox { } } +/// Causes the semantics of all siblings and cousins painted before it in the +/// same semantic container to be dropped. +/// +/// This is useful in a stack where an overlay should prevent interactions +/// with the underlying layers. +class RenderBlockSemantics extends RenderProxyBox { + RenderBlockSemantics({ RenderBox child }) : super(child); + + @override + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; +} + /// Causes the semantics of all descendants to be merged into this /// node such that the entire subtree becomes a single leaf in the /// semantics tree. diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 11d4c3ad9d..ce45f9cc13 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -3613,6 +3613,27 @@ class MergeSemantics extends SingleChildRenderObjectWidget { RenderMergeSemantics createRenderObject(BuildContext context) => new RenderMergeSemantics(); } +/// A widget that drops the semantics of all widget that were painted before it +/// in the same semantic container. +/// +/// This is useful to hide widgets from accessibility tools that are painted +/// behind a certain widget, e.g. an alert should usually disallow interaction +/// with any widget located "behind" the alert (even when they are still +/// partially visible). Similarly, an open [Drawer] blocks interactions with +/// any widget outside the drawer. +/// +/// See also: +/// +/// * [ExcludeSemantics] which drops all semantics of its descendants. +class BlockSemantics extends SingleChildRenderObjectWidget { + /// Creates a widget that excludes the semantics of all widgets painted before + /// it in the same semantic container. + const BlockSemantics({ Key key, Widget child }) : super(key: key, child: child); + + @override + RenderBlockSemantics createRenderObject(BuildContext context) => new RenderBlockSemantics(); +} + /// A widget that drops all the semantics of its descendants. /// /// When [excluding] is true, this widget (and its subtree) is excluded from @@ -3622,6 +3643,10 @@ class MergeSemantics extends SingleChildRenderObjectWidget { /// reported but that would only be confusing. For example, the /// material library's [Chip] widget hides the avatar since it is /// redundant with the chip label. +/// +/// See also: +/// +/// * [BlockSemantics] which drops semantics of widgets earlier in the tree. class ExcludeSemantics extends SingleChildRenderObjectWidget { /// Creates a widget that drops all the semantics of its descendants. const ExcludeSemantics({ diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index 0dbb979c40..76b79c3215 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -26,21 +26,23 @@ class ModalBarrier extends StatelessWidget { @override Widget build(BuildContext context) { - return new ExcludeSemantics( - excluding: !dismissible, - child: new Semantics( - container: true, - child: new GestureDetector( - onTapDown: (TapDownDetails details) { - if (dismissible) - Navigator.pop(context); - }, - behavior: HitTestBehavior.opaque, - child: new ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: color == null ? null : new DecoratedBox( - decoration: new BoxDecoration( - color: color + return new BlockSemantics( + child: new ExcludeSemantics( + excluding: !dismissible, + child: new Semantics( + container: true, + child: new GestureDetector( + onTapDown: (TapDownDetails details) { + if (dismissible) + Navigator.pop(context); + }, + behavior: HitTestBehavior.opaque, + child: new ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: color == null ? null : new DecoratedBox( + decoration: new BoxDecoration( + color: color + ) ) ) ) diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart index 83a8f28b39..d2e4f8dd74 100644 --- a/packages/flutter/test/material/dialog_test.dart +++ b/packages/flutter/test/material/dialog_test.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:matcher/matcher.dart'; + +import '../widgets/semantics_tester.dart'; void main() { testWidgets('Dialog is scrollable', (WidgetTester tester) async { @@ -192,4 +195,38 @@ void main() { expect(find.text('Dialog2'), findsOneWidget); }); + + testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + const String buttonText = 'A button covered by dialog overlay'; + await tester.pumpWidget( + new MaterialApp( + home: const Material( + child: const Center( + child: const RaisedButton( + onPressed: null, + child: const Text(buttonText), + ), + ), + ), + ), + ); + + expect(semantics, includesNodeWithLabel(buttonText)); + + final BuildContext context = tester.element(find.text(buttonText)); + + const String alertText = 'A button in an overlay alert'; + showDialog( + context: context, + child: const AlertDialog(title: const Text(alertText)), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(semantics, includesNodeWithLabel(alertText)); + expect(semantics, isNot(includesNodeWithLabel(buttonText))); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart index 219abe6c43..395589f030 100644 --- a/packages/flutter/test/material/scaffold_test.dart +++ b/packages/flutter/test/material/scaffold_test.dart @@ -6,6 +6,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import '../widgets/semantics_tester.dart'; + void main() { testWidgets('Scaffold control test', (WidgetTester tester) async { final Key bodyKey = new UniqueKey(); @@ -440,4 +442,40 @@ void main() { expect(tester.renderObject(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0)); }); }); + + testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async { + const String bodyLabel = 'I am the body'; + const String persistentFooterButtonLabel = 'a button on the bottom'; + const String bottomNavigationBarLabel = 'a bar in an app'; + const String floatingActionButtonLabel = 'I float in space'; + const String drawerLabel = 'I am the reason for this test'; + + final SemanticsTester semantics = new SemanticsTester(tester); + await tester.pumpWidget(new MaterialApp(home: new Scaffold( + body: new Semantics(label: bodyLabel, child: new Container()), + persistentFooterButtons: [new Semantics(label: persistentFooterButtonLabel, child: new Container())], + bottomNavigationBar: new Semantics(label: bottomNavigationBarLabel, child: new Container()), + floatingActionButton: new Semantics(label: floatingActionButtonLabel, child: new Container()), + drawer: new Drawer(child:new Semantics(label: drawerLabel, child: new Container())), + ))); + + expect(semantics, includesNodeWithLabel(bodyLabel)); + expect(semantics, includesNodeWithLabel(persistentFooterButtonLabel)); + expect(semantics, includesNodeWithLabel(bottomNavigationBarLabel)); + expect(semantics, includesNodeWithLabel(floatingActionButtonLabel)); + expect(semantics, isNot(includesNodeWithLabel(drawerLabel))); + + final ScaffoldState state = tester.firstState(find.byType(Scaffold)); + state.openDrawer(); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); + + expect(semantics, isNot(includesNodeWithLabel(bodyLabel))); + expect(semantics, isNot(includesNodeWithLabel(persistentFooterButtonLabel))); + expect(semantics, isNot(includesNodeWithLabel(bottomNavigationBarLabel))); + expect(semantics, isNot(includesNodeWithLabel(floatingActionButtonLabel))); + expect(semantics, includesNodeWithLabel(drawerLabel)); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/widgets/semantics_9_test.dart b/packages/flutter/test/widgets/semantics_9_test.dart new file mode 100644 index 0000000000..17eccc0bef --- /dev/null +++ b/packages/flutter/test/widgets/semantics_9_test.dart @@ -0,0 +1,149 @@ +// Copyright 2017 The Chromium 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/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'semantics_tester.dart'; + +void main() { + group('BlockSemantics', () { + testWidgets('hides semantic nodes of siblings', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: 'layer#1', + child: new Container(), + ), + const BlockSemantics(), + new Semantics( + label: 'layer#2', + child: new Container(), + ), + ], + )); + + expect(semantics, isNot(includesNodeWithLabel('layer#1'))); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: 'layer#1', + child: new Container(), + ), + ], + )); + + expect(semantics, includesNodeWithLabel('layer#1')); + + semantics.dispose(); + }); + + testWidgets('does not hides semantic nodes of siblings outside the current semantic boundary', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget(new Stack( + children: [ + new Semantics( + label: '#1', + child: new Container(), + ), + new Semantics( + label: '#2', + container: true, + child: new Stack( + children: [ + new Semantics( + label: 'NOT#2.1', + child: new Container(), + ), + new Semantics( + label: '#2.2', + child: new BlockSemantics( + child: new Semantics( + container: true, + label: '#2.2.1', + child: new Container(), + ), + ), + ), + new Semantics( + label: '#2.3', + child: new Container(), + ), + ], + ), + ), + new Semantics( + label: '#3', + child: new Container(), + ), + ], + )); + + expect(semantics, includesNodeWithLabel('#1')); + expect(semantics, includesNodeWithLabel('#2')); + expect(semantics, isNot(includesNodeWithLabel('NOT#2.1'))); + expect(semantics, includesNodeWithLabel('#2.2')); + expect(semantics, includesNodeWithLabel('#2.2.1')); + expect(semantics, includesNodeWithLabel('#2.3')); + expect(semantics, includesNodeWithLabel('#3')); + + semantics.dispose(); + }); + + testWidgets('node is semantic boundary and blocking previously painted nodes', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + final GlobalKey stackKey = new GlobalKey(); + + await tester.pumpWidget(new Stack( + key: stackKey, + children: [ + new Semantics( + label: 'NOT#1', + child: new Container(), + ), + new BoundaryBlockSemantics( + child: new Semantics( + label: '#2.1', + child: new Container(), + ) + ), + new Semantics( + label: '#3', + child: new Container(), + ), + ], + )); + + expect(semantics, isNot(includesNodeWithLabel('NOT#1'))); + expect(semantics, includesNodeWithLabel('#2.1')); + expect(semantics, includesNodeWithLabel('#3')); + + semantics.dispose(); + }); + }); +} + +class BoundaryBlockSemantics extends SingleChildRenderObjectWidget { + const BoundaryBlockSemantics({ Key key, Widget child }) : super(key: key, child: child); + + @override + RenderBoundaryBlockSemantics createRenderObject(BuildContext context) => new RenderBoundaryBlockSemantics(); +} + +class RenderBoundaryBlockSemantics extends RenderProxyBox { + RenderBoundaryBlockSemantics({ RenderBox child }) : super(child); + + @override + bool get isBlockingSemanticsOfPreviouslyPaintedNodes => true; + + @override + bool get isSemanticBoundary => true; +} + diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index eca1c757bd..e538cceaaf 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -194,6 +194,8 @@ class SemanticsTester { String toString() => 'SemanticsTester'; } +const String _matcherHelp = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.'; + class _HasSemantics extends Matcher { const _HasSemantics(this._semantics) : assert(_semantics != null); @@ -211,30 +213,65 @@ class _HasSemantics extends Matcher { @override Description describeMismatch(dynamic item, Description mismatchDescription, Map matchState, bool verbose) { - const String help = 'Try dumping the semantics with debugDumpSemanticsTree() from the rendering library to see what the semantics tree looks like.'; final TestSemantics testNode = matchState[TestSemantics]; final SemanticsNode node = matchState[SemanticsNode]; if (node == null) - return mismatchDescription.add('could not find node with id ${testNode.id}.\n$help'); + return mismatchDescription.add('could not find node with id ${testNode.id}.\n$_matcherHelp'); if (testNode.id != node.id) - return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}.\n$_matcherHelp'); final SemanticsData data = node.getSemanticsData(); if (testNode.flags != data.flags) - return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}.\n$_matcherHelp'); if (testNode.actions != data.actions) - return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}.\n$_matcherHelp'); if (testNode.label != data.label) - return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}".\n$_matcherHelp'); if (testNode.rect != data.rect) - return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}.\n$_matcherHelp'); if (testNode.transform != data.transform) - return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform:.\n${data.transform}.\n$_matcherHelp'); final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount; if (testNode.children.length != childrenCount) - return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$help'); + return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} children but found $childrenCount.\n$_matcherHelp'); return mismatchDescription; } } /// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics. Matcher hasSemantics(TestSemantics semantics) => new _HasSemantics(semantics); + +class _IncludesNodeWithLabel extends Matcher { + const _IncludesNodeWithLabel(this._label) : assert(_label != null); + + final String _label; + + @override + bool matches(covariant SemanticsTester item, Map matchState) { + bool result = false; + SemanticsNodeVisitor visitor; + visitor = (SemanticsNode node) { + if (node.label == _label) { + result = true; + } else { + node.visitChildren(visitor); + } + return !result; + }; + final SemanticsNode root = item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode; + visitor(root); + return result; + } + + @override + Description describe(Description description) { + return description.add('includes node with label "$_label"'); + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, Map matchState, bool verbose) { + return mismatchDescription.add('could not find node with label "$_label".\n$_matcherHelp'); + } +} + +/// Asserts that a node in the semantics tree of [SemanticsTester] has [label]. +Matcher includesNodeWithLabel(String label) => new _IncludesNodeWithLabel(label);