Support for accessibilityHint and accessibilityValue (#12677)

* Support accessibility labels and hints

* more tests

* ++

* review comments

* fix merge

* test fix
This commit is contained in:
Michael Goderbauer 2017-10-24 11:17:51 -07:00 committed by GitHub
parent 80b820a26d
commit c3e049613d
7 changed files with 393 additions and 58 deletions

View file

@ -1 +1 @@
8e79156765c67b71b1e1b9895dbc8eea43f9949c 91071f817b2f6a0f6684e1f2fda3d8b21314bcb7

View file

@ -3176,6 +3176,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool selected, bool selected,
bool button, bool button,
String label, String label,
String value,
String hint,
TextDirection textDirection, TextDirection textDirection,
}) : assert(container != null), }) : assert(container != null),
_container = container, _container = container,
@ -3184,6 +3186,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_selected = selected, _selected = selected,
_button = button, _button = button,
_label = label, _label = label,
_value = value,
_hint = hint,
_textDirection = textDirection, _textDirection = textDirection,
super(child); super(child);
@ -3250,17 +3254,6 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
} }
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
String get label => _label;
String _label;
set label(String value) {
if (label == value)
return;
final bool hadValue = label != null;
_label = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.isButton] semantic to the given value. /// If non-null, sets the [SemanticsNode.isButton] semantic to the given value.
bool get button => _button; bool get button => _button;
bool _button; bool _button;
@ -3272,9 +3265,48 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
} }
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get label => _label;
String _label;
set label(String value) {
if (_label == value)
return;
final bool hadValue = _label != null;
_label = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.value] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get value => _value;
String _value;
set value(String value) {
if (_value == value)
return;
final bool hadValue = _value != null;
_value = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.hint] semantic to the given value.
///
/// The text's reading direction is given by [textDirection].
String get hint => _hint;
String _hint;
set hint(String value) {
if (_hint == value)
return;
final bool hadValue = _hint != null;
_hint = value;
markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue);
}
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value. /// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value.
/// ///
/// This must not be null if [label] is not null. /// This must not be null if [label], [hint], or [value] is not null.
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection value) { set textDirection(TextDirection value) {
@ -3296,6 +3328,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isSelected = selected; config.isSelected = selected;
if (label != null) if (label != null)
config.label = label; config.label = label;
if (value != null)
config.value = value;
if (hint != null)
config.hint = hint;
if (textDirection != null) if (textDirection != null)
config.textDirection = textDirection; config.textDirection = textDirection;
if (button != null) if (button != null)

View file

@ -76,6 +76,8 @@ class SemanticsData extends Diagnosticable {
@required this.flags, @required this.flags,
@required this.actions, @required this.actions,
@required this.label, @required this.label,
@required this.value,
@required this.hint,
@required this.textDirection, @required this.textDirection,
@required this.rect, @required this.rect,
this.tags, this.tags,
@ -97,7 +99,17 @@ class SemanticsData extends Diagnosticable {
/// The text's reading direction is given by [textDirection]. /// The text's reading direction is given by [textDirection].
final String label; final String label;
/// The reading direction for the text in [label]. /// A textual description for the current value of the node.
///
/// The text's reading direction is given by [textDirection].
final String value;
/// A brief description of the result of performing an action on this node.
///
/// The text's reading direction is given by [textDirection].
final String hint;
/// The reading direction for the text in [label], [value], and [hint].
final TextDirection textDirection; final TextDirection textDirection;
/// The bounding box for this node in its coordinate system. /// The bounding box for this node in its coordinate system.
@ -485,6 +497,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) {
return _label != config.label || return _label != config.label ||
_hint != config.hint ||
_value != config.value ||
_flags != config._flags || _flags != config._flags ||
_textDirection != config.textDirection || _textDirection != config.textDirection ||
_actionsAsBits != config._actionsAsBits || _actionsAsBits != config._actionsAsBits ||
@ -516,7 +530,19 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
String get label => _label; String get label => _label;
String _label = _kEmptyConfig.label; String _label = _kEmptyConfig.label;
/// The reading direction for [label]. /// A textual description for the current value of the node.
///
/// The text's reading direction is given by [textDirection].
String get value => _value;
String _value = _kEmptyConfig.value;
/// A brief description of the result of performing an action on this node.
///
/// The text's reading direction is given by [textDirection].
String get hint => _hint;
String _hint = _kEmptyConfig.hint;
/// The reading direction for [label], [value], and [hint].
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection = _kEmptyConfig.textDirection; TextDirection _textDirection = _kEmptyConfig.textDirection;
@ -533,6 +559,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_markDirty(); _markDirty();
_label = config.label; _label = config.label;
_value = config.value;
_hint = config.hint;
_flags = config._flags; _flags = config._flags;
_textDirection = config.textDirection; _textDirection = config.textDirection;
_actions = new Map<SemanticsAction, VoidCallback>.from(config._actions); _actions = new Map<SemanticsAction, VoidCallback>.from(config._actions);
@ -551,6 +579,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
int flags = _flags; int flags = _flags;
int actions = _actionsAsBits; int actions = _actionsAsBits;
String label = _label; String label = _label;
String hint = _hint;
String value = _value;
TextDirection textDirection = _textDirection; TextDirection textDirection = _textDirection;
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags); Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
@ -560,27 +590,24 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
flags |= node._flags; flags |= node._flags;
actions |= node._actionsAsBits; actions |= node._actionsAsBits;
textDirection ??= node._textDirection; textDirection ??= node._textDirection;
if (value == '' || value == null)
value = node._value;
if (node.tags != null) { if (node.tags != null) {
mergedTags ??= new Set<SemanticsTag>(); mergedTags ??= new Set<SemanticsTag>();
mergedTags.addAll(node.tags); mergedTags.addAll(node.tags);
} }
if (node._label.isNotEmpty) { label = _concatStrings(
String nestedLabel = node._label; thisString: label,
if (textDirection != node._textDirection && node._textDirection != null) { thisTextDirection: textDirection,
switch (node._textDirection) { otherString: node._label,
case TextDirection.rtl: otherTextDirection: node._textDirection,
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; );
break; hint = _concatStrings(
case TextDirection.ltr: thisString: hint,
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; thisTextDirection: textDirection,
break; otherString: node._hint,
} otherTextDirection: node._textDirection,
} );
if (label.isEmpty)
label = nestedLabel;
else
label = '$label\n$nestedLabel';
}
return true; return true;
}); });
} }
@ -589,6 +616,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
flags: flags, flags: flags,
actions: actions, actions: actions,
label: label, label: label,
value: value,
hint: hint,
textDirection: textDirection, textDirection: textDirection,
rect: rect, rect: rect,
transform: transform, transform: transform,
@ -621,6 +650,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
actions: data.actions, actions: data.actions,
rect: data.rect, rect: data.rect,
label: data.label, label: data.label,
value: data.value,
hint: data.hint,
textDirection: data.textDirection, textDirection: data.textDirection,
transform: data.transform?.storage ?? _kIdentityTransform, transform: data.transform?.storage ?? _kIdentityTransform,
children: children, children: children,
@ -676,8 +707,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
if (_hasFlag(SemanticsFlags.hasCheckedState)) if (_hasFlag(SemanticsFlags.hasCheckedState))
properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked')); properties.add(new FlagProperty('isChecked', value: _hasFlag(SemanticsFlags.isChecked), ifTrue: 'checked', ifFalse: 'unchecked'));
properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected')); properties.add(new FlagProperty('isSelected', value: _hasFlag(SemanticsFlags.isSelected), ifTrue: 'selected'));
properties.add(new StringProperty('label', _label, defaultValue: ''));
properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button')); properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlags.isButton), ifTrue: 'button'));
properties.add(new StringProperty('label', _label, defaultValue: ''));
properties.add(new StringProperty('value', _value, defaultValue: ''));
properties.add(new StringProperty('hint', _hint, defaultValue: ''));
properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null)); properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
} }
@ -1015,6 +1048,11 @@ class SemanticsConfiguration {
/// A textual description of the owning [RenderObject]. /// A textual description of the owning [RenderObject].
/// ///
/// On iOS this is used for the `accessibilityLabel` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [value] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
///
/// The text's reading direction is given by [textDirection]. /// The text's reading direction is given by [textDirection].
String get label => _label; String get label => _label;
String _label = ''; String _label = '';
@ -1023,7 +1061,37 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// The reading direction for the text in [label]. /// A textual description for the current value of the owning [RenderObject].
///
/// On iOS this is used for the `accessibilityValue` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
///
/// The text's reading direction is given by [textDirection].
String get value => _value;
String _value = '';
set value(String value) {
_value = value;
_hasBeenAnnotated = true;
}
/// A brief description of the result of performing an action on this node.
///
/// On iOS this is used for the `accessibilityHint` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [value] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
///
/// The text's reading direction is given by [textDirection].
String get hint => _hint;
String _hint = '';
set hint(String hint) {
_hint = hint;
_hasBeenAnnotated = true;
}
/// The reading direction for the text in [label], [value], and [hint].
TextDirection get textDirection => _textDirection; TextDirection get textDirection => _textDirection;
TextDirection _textDirection; TextDirection _textDirection;
set textDirection(TextDirection textDirection) { set textDirection(TextDirection textDirection) {
@ -1087,6 +1155,8 @@ class SemanticsConfiguration {
return false; return false;
if ((_flags & other._flags) != 0) if ((_flags & other._flags) != 0)
return false; return false;
if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty)
return false;
return true; return true;
} }
@ -1109,23 +1179,20 @@ class SemanticsConfiguration {
_flags |= other._flags; _flags |= other._flags;
textDirection ??= other.textDirection; textDirection ??= other.textDirection;
if (other.label.isNotEmpty) { _label = _concatStrings(
String nestedLabel = other.label; thisString: _label,
if (textDirection != other.textDirection && other.textDirection != null) { thisTextDirection: textDirection,
switch (other.textDirection) { otherString: other._label,
case TextDirection.rtl: otherTextDirection: other.textDirection,
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}'; );
break; if (_value == '' || _value == null)
case TextDirection.ltr: _value = other._value;
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}'; _hint = _concatStrings(
break; thisString: _hint,
} thisTextDirection: textDirection,
} otherString: other._hint,
if (label.isEmpty) otherTextDirection: other.textDirection,
label = nestedLabel; );
else
label = '$label\n$nestedLabel';
}
_hasBeenAnnotated = _hasBeenAnnotated || other._hasBeenAnnotated; _hasBeenAnnotated = _hasBeenAnnotated || other._hasBeenAnnotated;
} }
@ -1138,8 +1205,34 @@ class SemanticsConfiguration {
.._hasBeenAnnotated = _hasBeenAnnotated .._hasBeenAnnotated = _hasBeenAnnotated
.._textDirection = _textDirection .._textDirection = _textDirection
.._label = _label .._label = _label
.._value = _value
.._hint = _hint
.._flags = _flags .._flags = _flags
.._actionsAsBits = _actionsAsBits .._actionsAsBits = _actionsAsBits
.._actions.addAll(_actions); .._actions.addAll(_actions);
} }
} }
String _concatStrings({
@required String thisString,
@required String otherString,
@required TextDirection thisTextDirection,
@required TextDirection otherTextDirection
}) {
if (otherString.isEmpty)
return thisString;
String nestedLabel = otherString;
if (thisTextDirection != otherTextDirection && otherTextDirection != null) {
switch (otherTextDirection) {
case TextDirection.rtl:
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}';
break;
case TextDirection.ltr:
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}';
break;
}
}
if (thisString.isEmpty)
return nestedLabel;
return '$thisString\n$nestedLabel';
}

View file

@ -4453,6 +4453,8 @@ class Semantics extends SingleChildRenderObjectWidget {
this.selected, this.selected,
this.button, this.button,
this.label, this.label,
this.value,
this.hint,
this.textDirection, this.textDirection,
}) : assert(container != null), }) : assert(container != null),
super(key: key, child: child); super(key: key, child: child);
@ -4502,15 +4504,40 @@ class Semantics extends SingleChildRenderObjectWidget {
/// ///
/// If a label is provided, there must either by an ambient [Directionality] /// If a label is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided. /// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.label] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String label; final String label;
/// The reading direction of the [label]. /// Provides a textual description of the value of the widget.
///
/// If a value is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.value] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String value;
/// Provides a brief textual description of the result of an action performed
/// on the widget.
///
/// If a hint is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// See also:
/// * [SemanticsConfiguration.hint] for a description of how this is exposed
/// in TalkBack and VoiceOver.
final String hint;
/// The reading direction of the [label], [value], and [hint].
/// ///
/// Defaults to the ambient [Directionality]. /// Defaults to the ambient [Directionality].
final TextDirection textDirection; final TextDirection textDirection;
TextDirection _getTextDirection(BuildContext context) { TextDirection _getTextDirection(BuildContext context) {
return textDirection ?? (label != null ? Directionality.of(context) : null); return textDirection ?? (label != null || value != null || hint != null ? Directionality.of(context) : null);
} }
@override @override
@ -4520,8 +4547,10 @@ class Semantics extends SingleChildRenderObjectWidget {
explicitChildNodes: explicitChildNodes, explicitChildNodes: explicitChildNodes,
checked: checked, checked: checked,
selected: selected, selected: selected,
label: label,
button: button, button: button,
label: label,
value: value,
hint: hint,
textDirection: _getTextDirection(context), textDirection: _getTextDirection(context),
); );
} }
@ -4534,6 +4563,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..checked = checked ..checked = checked
..selected = selected ..selected = selected
..label = label ..label = label
..value = value
..hint = hint
..textDirection = _getTextDirection(context); ..textDirection = _getTextDirection(context);
} }
@ -4544,6 +4575,8 @@ class Semantics extends SingleChildRenderObjectWidget {
description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null)); description.add(new DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
description.add(new DiagnosticsProperty<bool>('selected', selected, defaultValue: null)); description.add(new DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
description.add(new StringProperty('label', label, defaultValue: '')); description.add(new StringProperty('label', label, defaultValue: ''));
description.add(new StringProperty('value', value));
description.add(new StringProperty('hint', hint));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
} }
} }

View file

@ -198,7 +198,7 @@ void main() {
expect( expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden), minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, label: "", isButton: false, textDirection: null)\n', 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isButton: false, label: "", value: "", hint: "", textDirection: null)\n',
); );
final SemanticsConfiguration config = new SemanticsConfiguration() final SemanticsConfiguration config = new SemanticsConfiguration()
@ -217,7 +217,7 @@ void main() {
..updateWith(config: config, childrenInInversePaintOrder: null); ..updateWith(config: config, childrenInInversePaintOrder: null);
expect( expect(
allProperties.toStringDeep(), allProperties.toStringDeep(),
'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, label: "Use all the properties", button, textDirection: rtl)\n', 'SemanticsNode#17(STALE, owner: null, leaf merge, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl)\n',
); );
expect( expect(
allProperties.getSemanticsData().toString(), allProperties.getSemanticsData().toString(),

View file

@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -220,4 +221,153 @@ void main() {
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true)); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
}); });
testWidgets('Semantics label and hint', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
label: 'label',
hint: 'hint',
value: 'value',
child: new Container(),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
label: 'label',
hint: 'hint',
value: 'value',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics hints can merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
const Semantics(
hint: 'hint one',
),
const Semantics(
hint: 'hint two',
)
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
hint: 'hint one\nhint two',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics values do not merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
new Semantics(
value: 'value one',
child: new Container(
height: 10.0,
width: 10.0,
)
),
new Semantics(
value: 'value two',
child: new Container(
height: 10.0,
width: 10.0,
)
)
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
children: <TestSemantics>[
new TestSemantics(
value: 'value one',
textDirection: TextDirection.ltr,
),
new TestSemantics(
value: 'value two',
textDirection: TextDirection.ltr,
),
]
)
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
testWidgets('Semantics value and hint can merge', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
container: true,
child: new Column(
children: <Widget>[
const Semantics(
hint: 'hint',
),
const Semantics(
value: 'value',
),
],
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
hint: 'hint',
value: 'value',
textDirection: TextDirection.ltr,
)
]
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreRect: true, ignoreId: true));
});
} }

View file

@ -35,6 +35,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.value: '',
this.hint: '',
this.textDirection, this.textDirection,
this.rect, this.rect,
this.transform, this.transform,
@ -42,6 +44,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
@ -51,6 +55,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.value: '',
this.hint: '',
this.textDirection, this.textDirection,
this.transform, this.transform,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
@ -58,6 +64,8 @@ class TestSemantics {
}) : id = 0, }) : id = 0,
assert(flags != null), assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
rect = TestSemantics.rootRect, rect = TestSemantics.rootRect,
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
@ -76,6 +84,8 @@ class TestSemantics {
this.flags: 0, this.flags: 0,
this.actions: 0, this.actions: 0,
this.label: '', this.label: '',
this.hint: '',
this.value: '',
this.textDirection, this.textDirection,
this.rect, this.rect,
Matrix4 transform, Matrix4 transform,
@ -83,6 +93,8 @@ class TestSemantics {
Iterable<SemanticsTag> tags, Iterable<SemanticsTag> tags,
}) : assert(flags != null), }) : assert(flags != null),
assert(label != null), assert(label != null),
assert(value != null),
assert(hint != null),
transform = _applyRootChildScale(transform), transform = _applyRootChildScale(transform),
assert(children != null), assert(children != null),
tags = tags?.toSet() ?? new Set<SemanticsTag>(); tags = tags?.toSet() ?? new Set<SemanticsTag>();
@ -102,6 +114,13 @@ class TestSemantics {
/// A textual description of this node. /// A textual description of this node.
final String label; final String label;
/// A textual description for the value of this node.
final String value;
/// A brief textual description of the result of the action that can be
/// performed on this node.
final String hint;
/// The reading direction of the [label]. /// The reading direction of the [label].
/// ///
/// Even if this is not set, the [hasSemantics] matcher will verify that if a /// Even if this is not set, the [hasSemantics] matcher will verify that if a
@ -169,10 +188,14 @@ class TestSemantics {
return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.'); return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
if (label != nodeData.label) if (label != nodeData.label)
return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".'); return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
if (value != nodeData.value)
return fail('expected node id $id to have value "$value" but found value "${nodeData.value}".');
if (hint != nodeData.hint)
return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
if (textDirection != null && textDirection != nodeData.textDirection) if (textDirection != null && textDirection != nodeData.textDirection)
return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".'); return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
if (nodeData.label != '' && nodeData.textDirection == null) if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '') && nodeData.textDirection == null)
return fail('expected node id $id, which has a label, to have a textDirection, but it did not.'); return fail('expected node id $id, which has a label, value, or hint, to have a textDirection, but it did not.');
if (!ignoreRect && rect != nodeData.rect) if (!ignoreRect && rect != nodeData.rect)
return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.'); return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
if (!ignoreTransform && transform != nodeData.transform) if (!ignoreTransform && transform != nodeData.transform)