[a11y] add SemanticsValidationResult (#165935)

Add `SemanticsValidationResult` to semantics that maps onto
`aria-invalid`.

Fixes https://github.com/flutter/flutter/issues/162142
This commit is contained in:
Yegor
2025-04-02 15:00:19 -07:00
committed by GitHub
parent 10d2631548
commit fbbe0f9e7a
30 changed files with 620 additions and 23 deletions

View File

@@ -7,7 +7,6 @@
library;
import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

View File

@@ -912,6 +912,9 @@ class RenderCustomPaint extends RenderProxyBox {
final SemanticsProperties properties = newSemantics.properties;
final SemanticsConfiguration config = SemanticsConfiguration();
if (properties.role != null) {
config.role = properties.role!;
}
if (properties.sortKey != null) {
config.sortKey = properties.sortKey;
}
@@ -1017,6 +1020,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.textDirection != null) {
config.textDirection = properties.textDirection;
}
if (config.validationResult != properties.validationResult) {
config.validationResult = properties.validationResult;
}
if (properties.onTap != null) {
config.onTap = properties.onTap;
}

View File

@@ -4546,6 +4546,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
if (_properties.controlsNodes != null) {
config.controlsNodes = _properties.controlsNodes;
}
if (config.validationResult != _properties.validationResult) {
config.validationResult = _properties.validationResult;
}
// Registering _perform* as action handlers instead of the user provided
// ones to ensure that changing a user provided handler from a non-null to

View File

@@ -4,7 +4,6 @@
import 'dart:collection';
import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';

View File

@@ -19,6 +19,7 @@ import 'dart:ui'
SemanticsRole,
SemanticsUpdate,
SemanticsUpdateBuilder,
SemanticsValidationResult,
StringAttribute,
TextDirection;
@@ -32,7 +33,16 @@ import 'binding.dart' show SemanticsBinding;
import 'semantics_event.dart';
export 'dart:ui'
show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection, VoidCallback;
show
Offset,
Rect,
SemanticsAction,
SemanticsFlag,
SemanticsRole,
SemanticsValidationResult,
StringAttribute,
TextDirection,
VoidCallback;
export 'package:flutter/foundation.dart'
show
@@ -726,6 +736,7 @@ class SemanticsData with Diagnosticable {
required this.linkUrl,
required this.role,
required this.controlsNodes,
required this.validationResult,
this.tags,
this.transform,
this.customSemanticsActionIds,
@@ -991,6 +1002,9 @@ class SemanticsData with Diagnosticable {
/// {@macro flutter.semantics.SemanticsProperties.controlsNodes}
final Set<String>? controlsNodes;
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
final SemanticsValidationResult validationResult;
/// Whether [flags] contains the given flag.
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
@@ -1051,6 +1065,18 @@ class SemanticsData with Diagnosticable {
if (controlsNodes != null) {
properties.add(IterableProperty<String>('controls', controlsNodes, ifEmpty: null));
}
if (role != SemanticsRole.none) {
properties.add(EnumProperty<SemanticsRole>('role', role, defaultValue: SemanticsRole.none));
}
if (validationResult != SemanticsValidationResult.none) {
properties.add(
EnumProperty<SemanticsValidationResult>(
'validationResult',
validationResult,
defaultValue: SemanticsValidationResult.none,
),
);
}
}
@override
@@ -1083,6 +1109,7 @@ class SemanticsData with Diagnosticable {
other.headingLevel == headingLevel &&
other.linkUrl == linkUrl &&
other.role == role &&
other.validationResult == validationResult &&
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
setEquals<String>(controlsNodes, other.controlsNodes);
}
@@ -1118,6 +1145,7 @@ class SemanticsData with Diagnosticable {
linkUrl,
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
role,
validationResult,
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
),
);
@@ -1289,6 +1317,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.customSemanticsActions,
this.role,
this.controlsNodes,
this.validationResult = SemanticsValidationResult.none,
}) : assert(
label == null || attributedLabel == null,
'Only one of label or attributedLabel should be provided',
@@ -2097,7 +2126,7 @@ class SemanticsProperties extends DiagnosticableTree {
/// A enum to describe what role the subtree represents.
///
/// Setting the role for a widget subtree helps assistive technologies, such
/// as screen readers, understand and interact with the UI correctly.
/// as screen readers, to understand and interact with the UI correctly.
///
/// Defaults to [SemanticsRole.none] if not set, which means the subtree does
/// not represent any complex ui or controls.
@@ -2117,6 +2146,21 @@ class SemanticsProperties extends DiagnosticableTree {
/// {@endtemplate}
final Set<String>? controlsNodes;
/// {@template flutter.semantics.SemanticsProperties.validationResult}
/// Describes the validation result for a form field represented by this
/// widget.
///
/// Providing a validation result helps assistive technologies, such as screen
/// readers, to communicate to the user whether they provided correct
/// information in a form.
///
/// Defaults to [SemanticsValidationResult.none] if not set, which means no
/// validation information is available for the respective semantics node.
///
/// For a list of available validation results, see [SemanticsValidationResult].
/// {@endtemplate}
final SemanticsValidationResult validationResult;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
@@ -2155,6 +2199,13 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(StringProperty('tooltip', tooltip, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(EnumProperty<SemanticsRole>('role', role, defaultValue: null));
properties.add(
EnumProperty<SemanticsValidationResult>(
'validationResult',
validationResult,
defaultValue: SemanticsValidationResult.none,
),
);
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
properties.add(
DiagnosticsProperty<SemanticsHintOverrides>(
@@ -2744,7 +2795,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
_areUserActionsBlocked != config.isBlockingUserActions ||
_headingLevel != config._headingLevel ||
_linkUrl != config._linkUrl ||
_role != config.role;
_role != config.role ||
_validationResult != config.validationResult;
}
// TAGS, LABELS, ACTIONS
@@ -3083,6 +3135,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
Set<String>? get controlsNodes => _controlsNodes;
Set<String>? _controlsNodes = _kEmptyConfig.controlsNodes;
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
SemanticsValidationResult get validationResult => _validationResult;
SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult;
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
@@ -3150,6 +3206,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
_linkUrl = config._linkUrl;
_role = config._role;
_controlsNodes = config._controlsNodes;
_validationResult = config._validationResult;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
if (mergeAllDescendantsIntoThisNodeValueChanged) {
@@ -3200,6 +3257,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
Uri? linkUrl = _linkUrl;
SemanticsRole role = _role;
Set<String>? controlsNodes = _controlsNodes;
SemanticsValidationResult validationResult = _validationResult;
final Set<int> customSemanticsActionIds = <int>{};
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
@@ -3305,6 +3363,17 @@ class SemanticsNode with DiagnosticableTreeMixin {
controlsNodes = <String>{...controlsNodes!, ...node._controlsNodes!};
}
if (validationResult == SemanticsValidationResult.none) {
validationResult = node._validationResult;
} else if (validationResult == SemanticsValidationResult.valid) {
// When merging nodes, invalid validation result takes precedence.
// Otherwise, validation information could be lost.
if (node._validationResult != SemanticsValidationResult.none &&
node._validationResult != SemanticsValidationResult.valid) {
validationResult = node._validationResult;
}
}
return true;
});
}
@@ -3339,6 +3408,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
linkUrl: linkUrl,
role: role,
controlsNodes: controlsNodes,
validationResult: validationResult,
);
}
@@ -3425,6 +3495,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
linkUrl: data.linkUrl?.toString() ?? '',
role: data.role,
controlsNodes: data.controlsNodes?.toList(),
validationResult: data.validationResult,
);
_dirty = false;
}
@@ -5595,6 +5666,14 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
SemanticsValidationResult get validationResult => _validationResult;
SemanticsValidationResult _validationResult = SemanticsValidationResult.none;
set validationResult(SemanticsValidationResult value) {
_validationResult = value;
_hasBeenAnnotated = true;
}
// TAGS
/// The set of tags that this configuration wants to add to all child
@@ -5785,6 +5864,15 @@ class SemanticsConfiguration {
_controlsNodes = <String>{..._controlsNodes!, ...child._controlsNodes!};
}
if (child._validationResult != _validationResult) {
if (child._validationResult == SemanticsValidationResult.invalid) {
// Invalid result always takes precedence.
_validationResult = SemanticsValidationResult.invalid;
} else if (_validationResult == SemanticsValidationResult.none) {
_validationResult = child._validationResult;
}
}
_hasBeenAnnotated = hasBeenAnnotated || child.hasBeenAnnotated;
}
@@ -5827,7 +5915,8 @@ class SemanticsConfiguration {
.._headingLevel = _headingLevel
.._linkUrl = _linkUrl
.._role = _role
.._controlsNodes = _controlsNodes;
.._controlsNodes = _controlsNodes
.._validationResult = _validationResult;
}
}

View File

@@ -9,7 +9,7 @@
library;
import 'dart:math' as math;
import 'dart:ui' as ui show Image, ImageFilter, SemanticsRole, TextHeightBehavior;
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
@@ -7382,8 +7382,9 @@ class Semantics extends SingleChildRenderObjectWidget {
VoidCallback? onDidLoseAccessibilityFocus,
VoidCallback? onFocus,
Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions,
ui.SemanticsRole? role,
SemanticsRole? role,
Set<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
}) : this.fromProperties(
key: key,
child: child,
@@ -7461,6 +7462,7 @@ class Semantics extends SingleChildRenderObjectWidget {
: null,
role: role,
controlsNodes: controlsNodes,
validationResult: validationResult,
),
);

View File

@@ -769,6 +769,12 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
Form.maybeOf(context)?._register(this);
final Widget child = Semantics(
validationResult:
hasError ? SemanticsValidationResult.invalid : SemanticsValidationResult.valid,
child: widget.builder(this),
);
if (Form.maybeOf(context)?.widget.autovalidateMode == AutovalidateMode.onUnfocus &&
widget.autovalidateMode != AutovalidateMode.always ||
widget.autovalidateMode == AutovalidateMode.onUnfocus) {
@@ -783,11 +789,11 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
}
},
focusNode: _focusNode,
child: widget.builder(this),
child: child,
);
}
return widget.builder(this);
return child;
}
}

View File

@@ -7,7 +7,6 @@
library;
import 'dart:collection';
import 'dart:ui' show SemanticsRole;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

View File

@@ -6,7 +6,6 @@
library;
import 'dart:math' as math;
import 'dart:ui' show SemanticsRole;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

View File

@@ -1823,6 +1823,7 @@ void main() {
hasPasteAction: true,
hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorBackwardByWordAction: true,
validationResult: SemanticsValidationResult.valid,
),
);

View File

@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -306,6 +307,7 @@ void main() {
hasPasteAction: true,
hasMoveCursorBackwardByCharacterAction: true,
hasMoveCursorBackwardByWordAction: true,
validationResult: SemanticsValidationResult.valid,
),
);
semantics.dispose();

View File

@@ -228,8 +228,9 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
required Int32List additionalActions,
int headingLevel = 0,
String? linkUrl,
ui.SemanticsRole role = ui.SemanticsRole.none,
SemanticsRole role = SemanticsRole.none,
required List<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
}) {
// Makes sure we don't send the same id twice.
assert(!observations.containsKey(id));

View File

@@ -288,6 +288,94 @@ void _defineTests() {
semanticsTester.dispose();
});
testWidgets('provides semantic role', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(
CustomPaint(
foregroundPainter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
role: SemanticsRole.table,
label: 'this is a table',
textDirection: TextDirection.rtl,
),
),
),
),
);
expect(
semanticsTester,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 2,
role: SemanticsRole.table,
label: 'this is a table',
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
],
),
],
),
),
);
semanticsTester.dispose();
});
testWidgets('provides semantic validation result', (WidgetTester tester) async {
final SemanticsTester semanticsTester = SemanticsTester(tester);
await tester.pumpWidget(
CustomPaint(
foregroundPainter: _PainterWithSemantics(
semantics: const CustomPainterSemantics(
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
properties: SemanticsProperties(
textField: true,
label: 'email address',
textDirection: TextDirection.ltr,
validationResult: SemanticsValidationResult.invalid,
),
),
),
),
);
expect(
semanticsTester,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
TestSemantics(
id: 2,
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
label: 'email address',
validationResult: SemanticsValidationResult.invalid,
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
),
],
),
],
),
),
);
semanticsTester.dispose();
});
testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
await tester.pumpWidget(
CustomPaint(

View File

@@ -1539,4 +1539,53 @@ void main() {
expect(find.text('foo/error'), findsOneWidget);
expect(find.text('bar/error'), findsOneWidget);
});
testWidgets('FormField adds validation result to the semantics of the child', (
WidgetTester tester,
) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String? errorText;
Future<void> pumpWidget() async {
formKey.currentState?.reset();
await tester.pumpWidget(
MaterialApp(
home: MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: Form(
key: formKey,
autovalidateMode: AutovalidateMode.always,
child: TextFormField(validator: (String? value) => errorText),
),
),
),
),
),
),
);
await tester.enterText(find.byType(TextFormField), 'Hello');
await tester.pump();
}
// Test valid case
await pumpWidget();
expect(
tester.getSemantics(find.byType(TextFormField).last),
containsSemantics(isTextField: true, validationResult: SemanticsValidationResult.valid),
);
// Test invalid case
errorText = 'Error';
await pumpWidget();
expect(
tester.getSemantics(find.byType(TextFormField).last),
containsSemantics(isTextField: true, validationResult: SemanticsValidationResult.invalid),
);
});
}

View File

@@ -1698,6 +1698,127 @@ void main() {
semantics.dispose();
});
testWidgets('RenderSemanticsAnnotations provides validation result', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
Future<SemanticsConfiguration> pumpValidationResult(SemanticsValidationResult result) async {
final ValueKey<String> key = ValueKey<String>('validation-$result');
await tester.pumpWidget(
Semantics(
key: key,
validationResult: result,
child: Text('Validation result $result', textDirection: TextDirection.ltr),
),
);
final RenderSemanticsAnnotations object = tester.renderObject<RenderSemanticsAnnotations>(
find.byKey(key),
);
final SemanticsConfiguration config = SemanticsConfiguration();
object.describeSemanticsConfiguration(config);
return config;
}
final SemanticsConfiguration noneResult = await pumpValidationResult(
SemanticsValidationResult.none,
);
expect(noneResult.validationResult, SemanticsValidationResult.none);
final SemanticsConfiguration validResult = await pumpValidationResult(
SemanticsValidationResult.valid,
);
expect(validResult.validationResult, SemanticsValidationResult.valid);
final SemanticsConfiguration invalidResult = await pumpValidationResult(
SemanticsValidationResult.invalid,
);
expect(invalidResult.validationResult, SemanticsValidationResult.invalid);
semantics.dispose();
});
testWidgets('validation result precedence', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
Future<void> expectValidationResult({
required SemanticsValidationResult outer,
required SemanticsValidationResult inner,
required SemanticsValidationResult expected,
}) async {
const ValueKey<String> key = ValueKey<String>('validated-widget');
await tester.pumpWidget(
Semantics(
validationResult: outer,
child: Semantics(
validationResult: inner,
child: Text(
key: key,
'Outer = $outer; inner = $inner',
textDirection: TextDirection.ltr,
),
),
),
);
final SemanticsNode result = tester.getSemantics(find.byKey(key));
expect(
result,
containsSemantics(label: 'Outer = $outer; inner = $inner', validationResult: expected),
);
}
// Outer is none
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.none,
);
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.none,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
// Outer is valid
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.valid,
);
await expectValidationResult(
outer: SemanticsValidationResult.valid,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
// Outer is invalid
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.none,
expected: SemanticsValidationResult.invalid,
);
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.valid,
expected: SemanticsValidationResult.invalid,
);
await expectValidationResult(
outer: SemanticsValidationResult.invalid,
inner: SemanticsValidationResult.invalid,
expected: SemanticsValidationResult.invalid,
);
semantics.dispose();
});
}
class CustomSortKey extends OrdinalSortKey {

View File

@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
@@ -56,6 +54,7 @@ class TestSemantics {
this.scrollChildren,
Iterable<SemanticsTag>? tags,
this.role = SemanticsRole.none,
this.validationResult = SemanticsValidationResult.none,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
tags = tags?.toSet() ?? <SemanticsTag>{};
@@ -80,6 +79,7 @@ class TestSemantics {
this.scrollChildren,
Iterable<SemanticsTag>? tags,
this.role = SemanticsRole.none,
this.validationResult = SemanticsValidationResult.none,
}) : id = 0,
assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
@@ -120,6 +120,7 @@ class TestSemantics {
this.scrollChildren,
Iterable<SemanticsTag>? tags,
this.role = SemanticsRole.none,
this.validationResult = SemanticsValidationResult.none,
}) : assert(flags is int || flags is List<SemanticsFlag>),
assert(actions is int || actions is List<SemanticsAction>),
transform = _applyRootChildScale(transform),
@@ -232,6 +233,14 @@ class TestSemantics {
final TextSelection? textSelection;
/// The validation result for this node, if any.
///
/// See also:
///
/// * [SemanticsValidationResult], which is the enum listing possible values
/// for this field.
final SemanticsValidationResult validationResult;
static Matrix4 _applyRootChildScale(Matrix4? transform) {
final Matrix4 result = Matrix4.diagonal3Values(3.0, 3.0, 1.0);
if (transform != null) {
@@ -392,6 +401,12 @@ class TestSemantics {
return fail('expected node id $id to have role $role but found role ${node.role}');
}
if (validationResult != node.validationResult) {
return fail(
'expected node id $id to have validationResult $validationResult but found validationResult ${node.validationResult}',
);
}
if (children.isEmpty) {
return true;
}

View File

@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show SemanticsRole;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

View File

@@ -685,6 +685,7 @@ Matcher matchesSemantics({
int? platformViewId,
int? maxValueLength,
int? currentValueLength,
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
// Flags //
bool hasCheckedState = false,
bool isChecked = false,
@@ -768,6 +769,7 @@ Matcher matchesSemantics({
customActions: customActions,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
validationResult: validationResult,
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
@@ -879,6 +881,7 @@ Matcher containsSemantics({
int? platformViewId,
int? maxValueLength,
int? currentValueLength,
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
// Flags
bool? hasCheckedState,
bool? isChecked,
@@ -962,6 +965,7 @@ Matcher containsSemantics({
customActions: customActions,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
validationResult: validationResult,
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
@@ -2398,6 +2402,7 @@ class _MatchesSemanticsData extends Matcher {
required this.platformViewId,
required this.maxValueLength,
required this.currentValueLength,
required this.validationResult,
// Flags
required bool? hasCheckedState,
required bool? isChecked,
@@ -2552,6 +2557,7 @@ class _MatchesSemanticsData extends Matcher {
final int? maxValueLength;
final int? currentValueLength;
final List<Matcher>? children;
final SemanticsValidationResult validationResult;
/// There are three possible states for these two maps:
///
@@ -2665,6 +2671,9 @@ class _MatchesSemanticsData extends Matcher {
if (hintOverrides != null) {
description.add(' with custom hints: $hintOverrides');
}
if (validationResult != SemanticsValidationResult.none) {
description.add(' with validation result: $validationResult');
}
if (children != null) {
description.add(' with children:\n ');
final List<_MatchesSemanticsData> childMatches = children!.cast<_MatchesSemanticsData>();
@@ -2801,6 +2810,9 @@ class _MatchesSemanticsData extends Matcher {
if (maxValueLength != null && maxValueLength != data.maxValueLength) {
return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
}
if (validationResult != data.validationResult) {
return failWithDescription(matchState, 'validationResult was: ${data.validationResult}');
}
if (actions.isNotEmpty) {
final List<SemanticsAction> unexpectedActions = <SemanticsAction>[];
final List<SemanticsAction> missingActions = <SemanticsAction>[];

View File

@@ -731,6 +731,7 @@ void main() {
linkUrl: Uri(path: 'l'),
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@@ -1034,6 +1035,7 @@ void main() {
linkUrl: Uri(path: 'l'),
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@@ -1133,6 +1135,7 @@ void main() {
linkUrl: null,
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@@ -1239,6 +1242,7 @@ void main() {
linkUrl: null,
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
@@ -1271,6 +1275,7 @@ void main() {
linkUrl: Uri(path: 'l'),
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
@@ -1359,6 +1364,7 @@ void main() {
linkUrl: null,
role: ui.SemanticsRole.none,
controlsNodes: null,
validationResult: SemanticsValidationResult.none,
);
final _FakeSemanticsNode node = _FakeSemanticsNode(data);