mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This pull request fixes #143803 by taking advantage of Dart's null-aware operators. And unlike `switch` expressions ([9 PRs](https://github.com/flutter/flutter/pull/143634) and counting), the Flutter codebase is already fantastic when it comes to null-aware coding. After refactoring the entire repo, all the changes involving `?.` and `??` can fit into a single pull request.
234 lines
8.1 KiB
Dart
234 lines
8.1 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;
|
|
}
|
|
}
|