Commit 104725f3 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Adds a widgets that blocks all semantics of widgets below it in paint order...

Adds a widgets that blocks all semantics of widgets below it in paint order within the same container (#10425)
parent 417df36b
......@@ -270,10 +270,12 @@ class DrawerControllerState extends State<DrawerController> with SingleTickerPro
child: new RepaintBoundary(
child: new Stack(
children: <Widget>[
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(
......
......@@ -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 = <RenderObject>[renderObjectOwner];
......@@ -678,6 +679,9 @@ abstract class _SemanticsFragment {
}
final SemanticsAnnotator annotator;
bool dropSemanticsOfPreviousSiblings;
bool get producesSemanticNodes => true;
List<RenderObject> _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<SemanticsNode> 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
......
......@@ -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.
......
......@@ -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({
......
......@@ -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
)
)
)
)
......
......@@ -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<Null>(
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();
});
}
......@@ -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<RenderBox>(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: <Widget>[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();
});
}
// 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: <Widget>[
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: <Widget>[
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: <Widget>[
new Semantics(
label: '#1',
child: new Container(),
),
new Semantics(
label: '#2',
container: true,
child: new Stack(
children: <Widget>[
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: <Widget>[
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;
}
......@@ -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<dynamic, dynamic> 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<dynamic, dynamic> 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<dynamic, dynamic> 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);
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