flutter/dev/integration_tests/android_semantics_testing/lib/src/matcher.dart
Michael Goderbauer 5491c8c146
Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the
`engine` subdirectory and enforces that these files stay formatted with
a presubmit check.

**Reviewers:** Please carefully review all the commits except for the
one titled "formatted". The "formatted" commit was auto-generated by
running `dev/tools/format.sh -a -f`. The other commits were hand-crafted
to prepare the repo for the formatting change. I recommend reviewing the
commits one-by-one via the "Commits" tab and avoiding Github's "Files
changed" tab as it will likely slow down your browser because of the
size of this PR.

---------

Co-authored-by: Kate Lovett <katelovett@google.com>
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
2024-12-19 20:06:21 +00:00

254 lines
8.3 KiB
Dart

// Copyright 2014 The Flutter 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:test/test.dart' hide isInstanceOf;
import 'common.dart';
import 'constants.dart';
/// Matches an [AndroidSemanticsNode].
///
/// Any properties which aren't supplied are ignored during the comparison,
/// with the exception of `isHeading`. The heading property is not available
/// on all versions of Android. If it is not available on the tested version,
/// it will be match whatever it is compared against.
///
/// This matcher is intended to compare the accessibility values generated by
/// the Android accessibility bridge, and not the semantics object created by
/// the Flutter framework.
Matcher hasAndroidSemantics({
String? text,
String? contentDescription,
String? className,
int? id,
Rect? rect,
Size? size,
List<AndroidSemanticsAction>? actions,
List<AndroidSemanticsAction>? ignoredActions,
List<AndroidSemanticsNode>? children,
bool? isChecked,
bool? isCheckable,
bool? isEditable,
bool? isEnabled,
bool? isFocusable,
bool? isFocused,
bool? isHeading,
bool? isPassword,
bool? isLongClickable,
}) {
return _AndroidSemanticsMatcher(
text: text,
contentDescription: contentDescription,
className: className,
rect: rect,
size: size,
id: id,
actions: actions,
ignoredActions: ignoredActions,
isChecked: isChecked,
isCheckable: isCheckable,
isEditable: isEditable,
isEnabled: isEnabled,
isFocusable: isFocusable,
isFocused: isFocused,
isHeading: isHeading,
isPassword: isPassword,
isLongClickable: isLongClickable,
);
}
class _AndroidSemanticsMatcher extends Matcher {
_AndroidSemanticsMatcher({
this.text,
this.contentDescription,
this.className,
this.id,
this.actions,
this.ignoredActions,
this.rect,
this.size,
this.isChecked,
this.isCheckable,
this.isEnabled,
this.isEditable,
this.isFocusable,
this.isFocused,
this.isHeading,
this.isPassword,
this.isLongClickable,
}) : assert(
ignoredActions == null || actions != null,
'actions must not be null if ignoredActions is not null',
),
assert(ignoredActions == null || !actions!.any(ignoredActions.contains));
final String? text;
final String? className;
final String? contentDescription;
final int? id;
final List<AndroidSemanticsAction>? actions;
final List<AndroidSemanticsAction>? ignoredActions;
final Rect? rect;
final Size? size;
final bool? isChecked;
final bool? isCheckable;
final bool? isEditable;
final bool? isEnabled;
final bool? isFocusable;
final bool? isFocused;
final bool? isHeading;
final bool? isPassword;
final bool? isLongClickable;
@override
Description describe(Description description) {
description.add('AndroidSemanticsNode');
if (text != null) {
description.add(' with text: $text');
}
if (contentDescription != null) {
description.add('with contentDescription $contentDescription');
}
if (className != null) {
description.add(' with className: $className');
}
if (id != null) {
description.add(' with id: $id');
}
if (actions != null) {
description.add(' with actions: $actions');
}
if (ignoredActions != null) {
description.add(' with ignoredActions: $ignoredActions');
}
if (rect != null) {
description.add(' with rect: $rect');
}
if (size != null) {
description.add(' with size: $size');
}
if (isChecked != null) {
description.add(' with flag isChecked: $isChecked');
}
if (isEditable != null) {
description.add(' with flag isEditable: $isEditable');
}
if (isEnabled != null) {
description.add(' with flag isEnabled: $isEnabled');
}
if (isFocusable != null) {
description.add(' with flag isFocusable: $isFocusable');
}
if (isFocused != null) {
description.add(' with flag isFocused: $isFocused');
}
if (isHeading != null) {
description.add(' with flag isHeading: $isHeading');
}
if (isPassword != null) {
description.add(' with flag isPassword: $isPassword');
}
if (isLongClickable != null) {
description.add(' with flag isLongClickable: $isLongClickable');
}
return description;
}
@override
bool matches(covariant AndroidSemanticsNode item, Map<dynamic, dynamic> matchState) {
if (text != null && text != item.text) {
return _failWithMessage('Expected text: $text', matchState);
}
if (contentDescription != null && contentDescription != item.contentDescription) {
return _failWithMessage('Expected contentDescription: $contentDescription', matchState);
}
if (className != null && className != item.className) {
return _failWithMessage('Expected className: $className', matchState);
}
if (id != null && id != item.id) {
return _failWithMessage('Expected id: $id', matchState);
}
if (rect != null && rect != item.getRect()) {
return _failWithMessage('Expected rect: $rect', matchState);
}
if (size != null && size != item.getSize()) {
return _failWithMessage('Expected size: $size', matchState);
}
if (actions != null) {
final List<AndroidSemanticsAction> itemActions = item.getActions();
if (ignoredActions != null) {
itemActions.removeWhere(ignoredActions!.contains);
}
if (!unorderedEquals(actions!).matches(itemActions, matchState)) {
final List<String> actionsString =
actions!.map<String>((AndroidSemanticsAction action) => action.toString()).toList()
..sort();
final List<String> itemActionsString =
itemActions.map<String>((AndroidSemanticsAction action) => action.toString()).toList()
..sort();
final Set<String> unexpectedInString = itemActionsString.toSet().difference(
actionsString.toSet(),
);
final Set<String> missingInString = actionsString.toSet().difference(
itemActionsString.toSet(),
);
if (missingInString.isEmpty && unexpectedInString.isEmpty) {
return true;
}
return _failWithMessage(
'Expected actions: $actionsString\nActual actions: $itemActionsString\nUnexpected: $unexpectedInString\nMissing: $missingInString',
matchState,
);
}
}
if (isChecked != null && isChecked != item.isChecked) {
return _failWithMessage('Expected isChecked: $isChecked', matchState);
}
if (isCheckable != null && isCheckable != item.isCheckable) {
return _failWithMessage('Expected isCheckable: $isCheckable', matchState);
}
if (isEditable != null && isEditable != item.isEditable) {
return _failWithMessage('Expected isEditable: $isEditable', matchState);
}
if (isEnabled != null && isEnabled != item.isEnabled) {
return _failWithMessage('Expected isEnabled: $isEnabled', matchState);
}
if (isFocusable != null && isFocusable != item.isFocusable) {
return _failWithMessage('Expected isFocusable: $isFocusable', matchState);
}
if (isFocused != null && isFocused != item.isFocused) {
return _failWithMessage('Expected isFocused: $isFocused', matchState);
}
// Heading is not available in all Android versions, so match anything if it is not set by the platform
if (isHeading != null && isHeading != item.isHeading && item.isHeading != null) {
return _failWithMessage('Expected isHeading: $isHeading', matchState);
}
if (isPassword != null && isPassword != item.isPassword) {
return _failWithMessage('Expected isPassword: $isPassword', matchState);
}
if (isLongClickable != null && isLongClickable != item.isLongClickable) {
return _failWithMessage('Expected longClickable: $isLongClickable', matchState);
}
return true;
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
final String? failure = matchState['failure'] as String?;
return mismatchDescription.add(
failure ?? 'hasAndroidSemantics matcher does not complete successfully',
);
}
bool _failWithMessage(String value, Map<dynamic, dynamic> matchState) {
matchState['failure'] = value;
return false;
}
}