diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 7ac99b271c..c4aa4a1a27 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -3177,6 +3177,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { bool button, String label, String value, + String increasedValue, + String decreasedValue, String hint, TextDirection textDirection, VoidCallback onTap, @@ -3195,6 +3197,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { _button = button, _label = label, _value = value, + _increasedValue = increasedValue, + _decreasedValue = decreasedValue, _hint = hint, _textDirection = textDirection, _onTap = onTap, @@ -3283,7 +3287,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { /// If non-null, sets the [SemanticsNode.label] semantic to the given value. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. String get label => _label; String _label; set label(String value) { @@ -3296,7 +3300,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { /// If non-null, sets the [SemanticsNode.value] semantic to the given value. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. String get value => _value; String _value; set value(String value) { @@ -3307,9 +3311,37 @@ class RenderSemanticsAnnotations extends RenderProxyBox { markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); } + /// If non-null, sets the [SemanticsNode.increasedValue] semantic to the given + /// value. + /// + /// The reading direction is given by [textDirection]. + String get increasedValue => _increasedValue; + String _increasedValue; + set increasedValue(String value) { + if (_increasedValue == value) + return; + final bool hadValue = _increasedValue != null; + _increasedValue = value; + markNeedsSemanticsUpdate(onlyLocalUpdates: (value != null) == hadValue); + } + + /// If non-null, sets the [SemanticsNode.decreasedValue] semantic to the given + /// value. + /// + /// The reading direction is given by [textDirection]. + String get decreasedValue => _decreasedValue; + String _decreasedValue; + set decreasedValue(String value) { + if (_decreasedValue == value) + return; + final bool hadValue = _decreasedValue != null; + _decreasedValue = 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]. + /// The reading direction is given by [textDirection]. String get hint => _hint; String _hint; set hint(String value) { @@ -3322,7 +3354,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { /// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value. /// - /// This must not be null if [label], [hint], or [value] is not null. + /// This must not be null if [label], [hint], [value], [increasedValue], or + /// [decreasedValue] are not null. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection value) { @@ -3499,6 +3532,15 @@ class RenderSemanticsAnnotations extends RenderProxyBox { @override void describeSemanticsConfiguration(SemanticsConfiguration config) { + assert( + onIncrease == null || (value == null) == (increasedValue == null), + 'If "onIncrease" is set either both "value" and "increasedValue" or neither have to be set.', + ); + assert( + onDecrease == null || (value == null) == (decreasedValue == null), + 'If "onDecrease" is set either both "value" and "decreasedValue" or neither have to be set.', + ); + config.isSemanticBoundary = container; config.explicitChildNodes = explicitChildNodes; @@ -3512,6 +3554,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox { config.label = label; if (value != null) config.value = value; + if (increasedValue != null) + config.increasedValue = increasedValue; + if (decreasedValue != null) + config.decreasedValue = decreasedValue; if (hint != null) config.hint = hint; if (textDirection != null) diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index 347b93306a..edc5939521 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -76,7 +76,9 @@ class SemanticsData extends Diagnosticable { @required this.flags, @required this.actions, @required this.label, + @required this.increasedValue, @required this.value, + @required this.decreasedValue, @required this.hint, @required this.textDirection, @required this.rect, @@ -85,7 +87,15 @@ class SemanticsData extends Diagnosticable { }) : assert(flags != null), assert(actions != null), assert(label != null), + assert(value != null), + assert(decreasedValue != null), + assert(increasedValue != null), + assert(hint != null), assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'), + assert(value == '' || textDirection != null, 'A SemanticsData object with value "$value" had a null textDirection.'), + assert(hint == '' || textDirection != null, 'A SemanticsData object with hint "$hint" had a null textDirection.'), + assert(decreasedValue == '' || textDirection != null, 'A SemanticsData object with decreasedValue "$decreasedValue" had a null textDirection.'), + assert(increasedValue == '' || textDirection != null, 'A SemanticsData object with increasedValue "$increasedValue" had a null textDirection.'), assert(rect != null); /// A bit field of [SemanticsFlags] that apply to this node. @@ -96,20 +106,33 @@ class SemanticsData extends Diagnosticable { /// A textual description of this node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. final String label; /// A textual description for the current value of the node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. final String value; + /// The value that [value] will become after performing a + /// [SemanticsAction.increase] action. + /// + /// The reading direction is given by [textDirection]. + final String increasedValue; + + /// The value that [value] will become after performing a + /// [SemanticsAction.decrease] action. + /// + /// The reading direction is given by [textDirection]. + final String decreasedValue; + /// A brief description of the result of performing an action on this node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. final String hint; - /// The reading direction for the text in [label], [value], and [hint]. + /// The reading direction for the text in [label], [value], [hint], + /// [increasedValue], and [decreasedValue]. final TextDirection textDirection; /// The bounding box for this node in its coordinate system. @@ -153,6 +176,10 @@ class SemanticsData extends Diagnosticable { } properties.add(new IterableProperty('flags', flagSummary, ifEmpty: null)); properties.add(new StringProperty('label', label, defaultValue: '')); + properties.add(new StringProperty('value', value, defaultValue: '')); + properties.add(new StringProperty('increasedValue', increasedValue, defaultValue: '')); + properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: '')); + properties.add(new StringProperty('hint', hint, defaultValue: '')); properties.add(new EnumProperty('textDirection', textDirection, defaultValue: null)); } @@ -495,7 +522,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) { return _label != config.label || _hint != config.hint || + _decreasedValue != config.decreasedValue || _value != config.value || + _increasedValue != config.increasedValue || _flags != config._flags || _textDirection != config.textDirection || _actionsAsBits != config._actionsAsBits || @@ -523,23 +552,44 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// A textual description of this node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. String get label => _label; String _label = _kEmptyConfig.label; /// A textual description for the current value of the node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. String get value => _value; String _value = _kEmptyConfig.value; + /// The value that [value] will have after a [SemanticsAction.decrease] action + /// has been performed. + /// + /// This property is only valid if the [SemanticsAction.decrease] action is + /// available on this node. + /// + /// The reading direction is given by [textDirection]. + String get decreasedValue => _decreasedValue; + String _decreasedValue = _kEmptyConfig.decreasedValue; + + /// The value that [value] will have after a [SemanticsAction.increase] action + /// has been performed. + /// + /// This property is only valid if the [SemanticsAction.increase] action is + /// available on this node. + /// + /// The reading direction is given by [textDirection]. + String get increasedValue => _increasedValue; + String _increasedValue = _kEmptyConfig.increasedValue; + /// A brief description of the result of performing an action on this node. /// - /// The text's reading direction is given by [textDirection]. + /// The reading direction is given by [textDirection]. String get hint => _hint; String _hint = _kEmptyConfig.hint; - /// The reading direction for [label], [value], and [hint]. + /// The reading direction for [label], [value], [hint], [increasedValue], and + /// [decreasedValue]. TextDirection get textDirection => _textDirection; TextDirection _textDirection = _kEmptyConfig.textDirection; @@ -556,7 +606,9 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { _markDirty(); _label = config.label; + _decreasedValue = config.decreasedValue; _value = config.value; + _increasedValue = config.increasedValue; _hint = config.hint; _flags = config._flags; _textDirection = config.textDirection; @@ -564,6 +616,15 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { _actionsAsBits = config._actionsAsBits; _mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants; _replaceChildren(childrenInInversePaintOrder ?? const []); + + assert( + !_canPerformAction(SemanticsAction.increase) || (_value == '') == (_increasedValue == ''), + 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither', + ); + assert( + !_canPerformAction(SemanticsAction.decrease) || (_value == '') == (_decreasedValue == ''), + 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "decreasedValue" or neither', + ); } @@ -578,6 +639,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { String label = _label; String hint = _hint; String value = _value; + String increasedValue = _increasedValue; + String decreasedValue = _decreasedValue; TextDirection textDirection = _textDirection; Set mergedTags = tags == null ? null : new Set.from(tags); @@ -589,6 +652,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { textDirection ??= node._textDirection; if (value == '' || value == null) value = node._value; + if (increasedValue == '' || increasedValue == null) + increasedValue = node._increasedValue; + if (decreasedValue == '' || decreasedValue == null) + decreasedValue = node._decreasedValue; if (node.tags != null) { mergedTags ??= new Set(); mergedTags.addAll(node.tags); @@ -614,6 +681,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { actions: actions, label: label, value: value, + increasedValue: increasedValue, + decreasedValue: decreasedValue, hint: hint, textDirection: textDirection, rect: rect, @@ -648,6 +717,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { rect: data.rect, label: data.label, value: data.value, + decreasedValue: data.decreasedValue, + increasedValue: data.increasedValue, hint: data.hint, textDirection: data.textDirection, transform: data.transform?.storage ?? _kIdentityTransform, @@ -707,6 +778,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { 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('increasedValue', _increasedValue, defaultValue: '')); + properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: '')); properties.add(new StringProperty('hint', _hint, defaultValue: '')); properties.add(new EnumProperty('textDirection', _textDirection, defaultValue: null)); } @@ -1018,6 +1091,7 @@ class SemanticsConfiguration { /// /// Whenever the user performs `action` the provided `handler` is called. void addAction(SemanticsAction action, VoidCallback handler) { + assert(handler != null); _actions[action] = handler; _actionsAsBits |= action.index; _hasBeenAnnotated = true; @@ -1050,10 +1124,11 @@ class SemanticsConfiguration { /// [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 reading direction is given by [textDirection]. String get label => _label; String _label = ''; set label(String label) { + assert(label != null); _label = label; _hasBeenAnnotated = true; } @@ -1065,14 +1140,51 @@ class SemanticsConfiguration { /// [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]. + /// The reading direction is given by [textDirection]. + /// + /// See also: + /// * [decreasedValue], describes what [value] will be after performing + /// [SemanticsAction.decrease] + /// * [increasedValue], describes what [value] will be after performing + /// [SemanticsAction.increase] String get value => _value; String _value = ''; set value(String value) { + assert(value != null); _value = value; _hasBeenAnnotated = true; } + /// The value that [value] will have after performing a + /// [SemanticsAction.decrease] action. + /// + /// This must be set if a handler for [SemanticsAction.decrease] is provided + /// and [value] is set. + /// + /// The reading direction is given by [textDirection]. + String get decreasedValue => _decreasedValue; + String _decreasedValue = ''; + set decreasedValue(String decreasedValue) { + assert(decreasedValue != null); + _decreasedValue = decreasedValue; + _hasBeenAnnotated = true; + } + + /// The value that [value] will have after performing a + /// [SemanticsAction.increase] action. + /// + /// This must be set if a handler for [SemanticsAction.increase] is provided + /// and [value] is set. + /// + /// The reading direction is given by [textDirection]. + String get increasedValue => _increasedValue; + String _increasedValue = ''; + set increasedValue(String increasedValue) { + assert(increasedValue != null); + _increasedValue = increasedValue; + _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 @@ -1080,15 +1192,17 @@ class SemanticsConfiguration { /// [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]. + /// The reading direction is given by [textDirection]. String get hint => _hint; String _hint = ''; set hint(String hint) { + assert(hint != null); _hint = hint; _hasBeenAnnotated = true; } - /// The reading direction for the text in [label], [value], and [hint]. + /// The reading direction for the text in [label], [value], [hint], + /// [increasedValue], and [decreasedValue]. TextDirection get textDirection => _textDirection; TextDirection _textDirection; set textDirection(TextDirection textDirection) { @@ -1182,8 +1296,12 @@ class SemanticsConfiguration { otherString: other._label, otherTextDirection: other.textDirection, ); + if (_decreasedValue == '' || _decreasedValue == null) + _decreasedValue = other._decreasedValue; if (_value == '' || _value == null) _value = other._value; + if (_increasedValue == '' || _increasedValue == null) + _increasedValue = other._increasedValue; _hint = _concatStrings( thisString: _hint, thisTextDirection: textDirection, @@ -1202,7 +1320,9 @@ class SemanticsConfiguration { .._hasBeenAnnotated = _hasBeenAnnotated .._textDirection = _textDirection .._label = _label + .._increasedValue = _increasedValue .._value = _value + .._decreasedValue = _decreasedValue .._hint = _hint .._flags = _flags .._actionsAsBits = _actionsAsBits diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 9126c972f5..a000c6a39b 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4454,6 +4454,8 @@ class Semantics extends SingleChildRenderObjectWidget { this.button, this.label, this.value, + this.increasedValue, + this.decreasedValue, this.hint, this.textDirection, this.onTap, @@ -4528,6 +4530,30 @@ class Semantics extends SingleChildRenderObjectWidget { /// in TalkBack and VoiceOver. final String value; + /// The value that [value] will become after a [SemanticsAction.increase] + /// action has been performed on this widget. + /// + /// If a value is provided, [onIncrease] must also be set and there must + /// either be an ambient [Directionality] or an explicit [textDirection] + /// must be provided. + /// + /// See also: + /// * [SemanticsConfiguration.increasedValue] for a description of how this + /// is exposed in TalkBack and VoiceOver. + final String increasedValue; + + /// The value that [value] will become after a [SemanticsAction.decrease] + /// action has been performed on this widget. + /// + /// If a value is provided, [onDecrease] must also be set and there must + /// either be an ambient [Directionality] or an explicit [textDirection] + /// must be provided. + /// + /// See also: + /// * [SemanticsConfiguration.decreasedValue] for a description of how this + /// is exposed in TalkBack and VoiceOver. + final String decreasedValue; + /// Provides a brief textual description of the result of an action performed /// on the widget. /// @@ -4539,7 +4565,8 @@ class Semantics extends SingleChildRenderObjectWidget { /// in TalkBack and VoiceOver. final String hint; - /// The reading direction of the [label], [value], and [hint]. + /// The reading direction of the [label], [value], [hint], [increasedValue], + /// and [decreasedValue]. /// /// Defaults to the ambient [Directionality]. final TextDirection textDirection; @@ -4625,6 +4652,9 @@ class Semantics extends SingleChildRenderObjectWidget { /// This is a request to increase the value represented by the widget. For /// example, this action might be recognized by a slider control. /// + /// If a [value] is set, [increasedValue] must also be provided and + /// [onIncrease] must ensure that [value] will be set to [increasedValue]. + /// /// VoiceOver users on iOS can trigger this action by swiping up with one /// finger. TalkBack users on Android can trigger this action by pressing the /// volume up button. @@ -4635,6 +4665,9 @@ class Semantics extends SingleChildRenderObjectWidget { /// This is a request to decrease the value represented by the widget. For /// example, this action might be recognized by a slider control. /// + /// If a [value] is set, [decreasedValue] must also be provided and + /// [onDecrease] must ensure that [value] will be set to [decreasedValue]. + /// /// VoiceOver users on iOS can trigger this action by swiping down with one /// finger. TalkBack users on Android can trigger this action by pressing the /// volume down button. @@ -4650,6 +4683,8 @@ class Semantics extends SingleChildRenderObjectWidget { button: button, label: label, value: value, + increasedValue: increasedValue, + decreasedValue: decreasedValue, hint: hint, textDirection: _getTextDirection(context), onTap: onTap, @@ -4672,6 +4707,8 @@ class Semantics extends SingleChildRenderObjectWidget { ..selected = selected ..label = label ..value = value + ..increasedValue = increasedValue + ..decreasedValue = decreasedValue ..hint = hint ..textDirection = _getTextDirection(context) ..onTap = onTap diff --git a/packages/flutter/test/rendering/semantics_test.dart b/packages/flutter/test/rendering/semantics_test.dart index e06c6cc877..22373dff76 100644 --- a/packages/flutter/test/rendering/semantics_test.dart +++ b/packages/flutter/test/rendering/semantics_test.dart @@ -198,7 +198,7 @@ void main() { expect( minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden), - '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', + 'SemanticsNode#16(owner: null, isPartOfNodeMerging: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isSelected: false, isButton: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n', ); final SemanticsConfiguration config = new SemanticsConfiguration() diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart index 120ef7093b..5e9c916f29 100644 --- a/packages/flutter/test/widgets/semantics_test.dart +++ b/packages/flutter/test/widgets/semantics_test.dart @@ -513,4 +513,36 @@ void main() { semantics.dispose(); }); + + testWidgets('Increased/decreased values are annotated', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: new Semantics( + container: true, + value: '10s', + increasedValue: '11s', + decreasedValue: '9s', + onIncrease: () => () {}, + onDecrease: () => () {}, + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + actions: SemanticsAction.increase.index | SemanticsAction.decrease.index, + textDirection: TextDirection.ltr, + value: '10s', + increasedValue: '11s', + decreasedValue: '9s', + ), + ], + ), ignoreTransform: true, ignoreRect: true, ignoreId: true)); + + semantics.dispose(); + }); } diff --git a/packages/flutter/test/widgets/semantics_tester.dart b/packages/flutter/test/widgets/semantics_tester.dart index 5656e5561c..0e2437dfac 100644 --- a/packages/flutter/test/widgets/semantics_tester.dart +++ b/packages/flutter/test/widgets/semantics_tester.dart @@ -36,6 +36,8 @@ class TestSemantics { this.actions: 0, this.label: '', this.value: '', + this.increasedValue: '', + this.decreasedValue: '', this.hint: '', this.textDirection, this.rect, @@ -45,6 +47,8 @@ class TestSemantics { }) : assert(flags != null), assert(label != null), assert(value != null), + assert(increasedValue != null), + assert(decreasedValue != null), assert(hint != null), assert(children != null), tags = tags?.toSet() ?? new Set(); @@ -56,6 +60,8 @@ class TestSemantics { this.actions: 0, this.label: '', this.value: '', + this.increasedValue: '', + this.decreasedValue: '', this.hint: '', this.textDirection, this.transform, @@ -64,6 +70,8 @@ class TestSemantics { }) : id = 0, assert(flags != null), assert(label != null), + assert(increasedValue != null), + assert(decreasedValue != null), assert(value != null), assert(hint != null), rect = TestSemantics.rootRect, @@ -86,6 +94,8 @@ class TestSemantics { this.label: '', this.hint: '', this.value: '', + this.increasedValue: '', + this.decreasedValue: '', this.textDirection, this.rect, Matrix4 transform, @@ -94,6 +104,8 @@ class TestSemantics { }) : assert(flags != null), assert(label != null), assert(value != null), + assert(increasedValue != null), + assert(decreasedValue != null), assert(hint != null), transform = _applyRootChildScale(transform), assert(children != null), @@ -117,6 +129,14 @@ class TestSemantics { /// A textual description for the value of this node. final String value; + /// What [value] will become after [SemanticsAction.increase] has been + /// performed. + final String increasedValue; + + /// What [value] will become after [SemanticsAction.decrease] has been + /// performed. + final String decreasedValue; + /// A brief textual description of the result of the action that can be /// performed on this node. final String hint; @@ -190,11 +210,15 @@ class TestSemantics { 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 (increasedValue != nodeData.increasedValue) + return fail('expected node id $id to have increasedValue "$increasedValue" but found value "${nodeData.increasedValue}".'); + if (decreasedValue != nodeData.decreasedValue) + return fail('expected node id $id to have decreasedValue "$decreasedValue" but found value "${nodeData.decreasedValue}".'); 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) return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".'); - if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '') && nodeData.textDirection == null) + if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '' || node.increasedValue != '' || node.decreasedValue != '') && nodeData.textDirection == null) 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) return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');