mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
This commit is contained in:
parent
f19f040a73
commit
4373a31971
@ -64,7 +64,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('TextField has correct Android semantics', () async {
|
||||
final SerializableFinder normalTextField = find.byValueKey(normalTextFieldKeyValue);
|
||||
final SerializableFinder normalTextField = find.descendant(
|
||||
of: find.byValueKey(normalTextFieldKeyValue),
|
||||
matching: find.byType('Semantics'),
|
||||
firstMatchOnly: true,
|
||||
);
|
||||
expect(await getSemantics(normalTextField), hasAndroidSemantics(
|
||||
className: AndroidClassName.editText,
|
||||
isEditable: true,
|
||||
@ -112,7 +116,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('password TextField has correct Android semantics', () async {
|
||||
final SerializableFinder passwordTextField = find.byValueKey(passwordTextFieldKeyValue);
|
||||
final SerializableFinder passwordTextField = find.descendant(
|
||||
of: find.byValueKey(passwordTextFieldKeyValue),
|
||||
matching: find.byType('Semantics'),
|
||||
firstMatchOnly: true,
|
||||
);
|
||||
expect(await getSemantics(passwordTextField), hasAndroidSemantics(
|
||||
className: AndroidClassName.editText,
|
||||
isEditable: true,
|
||||
|
@ -753,6 +753,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
|
||||
bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;
|
||||
|
||||
int get _currentLength => _effectiveController.value.text.runes.length;
|
||||
|
||||
InputDecoration _getEffectiveDecoration() {
|
||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||
final ThemeData themeData = Theme.of(context);
|
||||
@ -769,7 +771,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
|
||||
// If buildCounter was provided, use it to generate a counter widget.
|
||||
Widget counter;
|
||||
final int currentLength = _effectiveController.value.text.runes.length;
|
||||
final int currentLength = _currentLength;
|
||||
if (effectiveDecoration.counter == null
|
||||
&& effectiveDecoration.counterText == null
|
||||
&& widget.buildCounter != null) {
|
||||
@ -1099,18 +1101,27 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
onTap: () {
|
||||
if (!_effectiveController.selection.isValid)
|
||||
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
|
||||
_requestKeyboard();
|
||||
},
|
||||
return IgnorePointer(
|
||||
ignoring: !_isEnabled,
|
||||
child: MouseRegion(
|
||||
onEnter: _handleMouseEnter,
|
||||
onExit: _handleMouseExit,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isEnabled,
|
||||
child: AnimatedBuilder(
|
||||
animation: controller, // changes the _currentLength
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return Semantics(
|
||||
maxValueLength: widget.maxLengthEnforced && widget.maxLength != null && widget.maxLength > 0
|
||||
? widget.maxLength
|
||||
: null,
|
||||
currentValueLength: _currentLength,
|
||||
onTap: () {
|
||||
if (!_effectiveController.selection.isValid)
|
||||
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length);
|
||||
_requestKeyboard();
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: child,
|
||||
|
@ -864,6 +864,12 @@ class RenderCustomPaint extends RenderProxyBox {
|
||||
if (properties.liveRegion != null) {
|
||||
config.liveRegion = properties.liveRegion;
|
||||
}
|
||||
if (properties.maxValueLength != null) {
|
||||
config.maxValueLength = properties.maxValueLength;
|
||||
}
|
||||
if (properties.currentValueLength != null) {
|
||||
config.currentValueLength = properties.currentValueLength;
|
||||
}
|
||||
if (properties.toggled != null) {
|
||||
config.isToggled = properties.toggled;
|
||||
}
|
||||
|
@ -3497,6 +3497,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
bool hidden,
|
||||
bool image,
|
||||
bool liveRegion,
|
||||
int maxValueLength,
|
||||
int currentValueLength,
|
||||
String label,
|
||||
String value,
|
||||
String increasedValue,
|
||||
@ -3544,6 +3546,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
_scopesRoute = scopesRoute,
|
||||
_namesRoute = namesRoute,
|
||||
_liveRegion = liveRegion,
|
||||
_maxValueLength = maxValueLength,
|
||||
_currentValueLength = currentValueLength,
|
||||
_hidden = hidden,
|
||||
_image = image,
|
||||
_onDismiss = onDismiss,
|
||||
@ -3799,6 +3803,28 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// If non-null, sets the [SemanticsNode.maxValueLength] semantic to the given
|
||||
/// value.
|
||||
int get maxValueLength => _maxValueLength;
|
||||
int _maxValueLength;
|
||||
set maxValueLength(int value) {
|
||||
if (_maxValueLength == value)
|
||||
return;
|
||||
_maxValueLength = value;
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// If non-null, sets the [SemanticsNode.currentValueLength] semantic to the
|
||||
/// given value.
|
||||
int get currentValueLength => _currentValueLength;
|
||||
int _currentValueLength;
|
||||
set currentValueLength(int value) {
|
||||
if (_currentValueLength == value)
|
||||
return;
|
||||
_currentValueLength = value;
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// If non-null, sets the [SemanticsNode.isToggled] semantic to the given
|
||||
/// value.
|
||||
bool get toggled => _toggled;
|
||||
@ -4370,6 +4396,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
config.namesRoute = namesRoute;
|
||||
if (liveRegion != null)
|
||||
config.liveRegion = liveRegion;
|
||||
if (maxValueLength != null) {
|
||||
config.maxValueLength = maxValueLength;
|
||||
}
|
||||
if (currentValueLength != null) {
|
||||
config.currentValueLength = currentValueLength;
|
||||
}
|
||||
if (textDirection != null)
|
||||
config.textDirection = textDirection;
|
||||
if (sortKey != null)
|
||||
|
@ -197,6 +197,8 @@ class SemanticsData extends Diagnosticable {
|
||||
@required this.scrollExtentMax,
|
||||
@required this.scrollExtentMin,
|
||||
@required this.platformViewId,
|
||||
@required this.maxValueLength,
|
||||
@required this.currentValueLength,
|
||||
this.tags,
|
||||
this.transform,
|
||||
this.customSemanticsActionIds,
|
||||
@ -309,6 +311,26 @@ class SemanticsData extends Diagnosticable {
|
||||
/// * [UiKitView], which is the platform view for iOS.
|
||||
final int platformViewId;
|
||||
|
||||
/// The maximum number of characters that can be entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [SemanticsFlag.isTextField] is set. Defaults
|
||||
/// to null, which means no limit is imposed on the text field.
|
||||
final int maxValueLength;
|
||||
|
||||
/// The current number of characters that have been entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [SemanticsFlag.isTextField] is set. This must
|
||||
/// be set when [maxValueLength] is set.
|
||||
final int currentValueLength;
|
||||
|
||||
/// The bounding box for this node in its coordinate system.
|
||||
final Rect rect;
|
||||
|
||||
@ -389,6 +411,8 @@ class SemanticsData extends Diagnosticable {
|
||||
if (textSelection?.isValid == true)
|
||||
properties.add(MessageProperty('textSelection', '[${textSelection.start}, ${textSelection.end}]'));
|
||||
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
|
||||
properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null));
|
||||
properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null));
|
||||
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
|
||||
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
|
||||
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
|
||||
@ -418,6 +442,8 @@ class SemanticsData extends Diagnosticable {
|
||||
&& typedOther.scrollExtentMax == scrollExtentMax
|
||||
&& typedOther.scrollExtentMin == scrollExtentMin
|
||||
&& typedOther.platformViewId == platformViewId
|
||||
&& typedOther.maxValueLength == maxValueLength
|
||||
&& typedOther.currentValueLength == currentValueLength
|
||||
&& typedOther.transform == transform
|
||||
&& typedOther.elevation == elevation
|
||||
&& typedOther.thickness == thickness
|
||||
@ -445,10 +471,12 @@ class SemanticsData extends Diagnosticable {
|
||||
scrollExtentMax,
|
||||
scrollExtentMin,
|
||||
platformViewId,
|
||||
maxValueLength,
|
||||
currentValueLength,
|
||||
transform,
|
||||
elevation,
|
||||
thickness,
|
||||
),
|
||||
elevation,
|
||||
thickness,
|
||||
ui.hashList(customSemanticsActionIds),
|
||||
);
|
||||
}
|
||||
@ -575,6 +603,8 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
this.namesRoute,
|
||||
this.image,
|
||||
this.liveRegion,
|
||||
this.maxValueLength,
|
||||
this.currentValueLength,
|
||||
this.label,
|
||||
this.value,
|
||||
this.increasedValue,
|
||||
@ -760,6 +790,26 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// * [UpdateLiveRegionEvent], to trigger a polite announcement of a live region.
|
||||
final bool liveRegion;
|
||||
|
||||
/// The maximum number of characters that can be entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [textField] is true. Defaults to null,
|
||||
/// which means no limit is imposed on the text field.
|
||||
final int maxValueLength;
|
||||
|
||||
/// The current number of characters that have been entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [textField] is true. Must be set when
|
||||
/// [maxValueLength] is set.
|
||||
final int currentValueLength;
|
||||
|
||||
/// Provides a textual description of the widget.
|
||||
///
|
||||
/// If a label is provided, there must either by an ambient [Directionality]
|
||||
@ -1512,6 +1562,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
_actionsAsBits != config._actionsAsBits ||
|
||||
indexInParent != config.indexInParent ||
|
||||
platformViewId != config.platformViewId ||
|
||||
_maxValueLength != config._maxValueLength ||
|
||||
_currentValueLength != config._currentValueLength ||
|
||||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
|
||||
}
|
||||
|
||||
@ -1729,6 +1781,28 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
int get platformViewId => _platformViewId;
|
||||
int _platformViewId;
|
||||
|
||||
/// The maximum number of characters that can be entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [SemanticsFlag.isTextField] is set. Defaults
|
||||
/// to null, which means no limit is imposed on the text field.
|
||||
int get maxValueLength => _maxValueLength;
|
||||
int _maxValueLength;
|
||||
|
||||
/// The current number of characters that have been entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [SemanticsFlag.isTextField] is set. Must be
|
||||
/// set when [maxValueLength] is set.
|
||||
int get currentValueLength => _currentValueLength;
|
||||
int _currentValueLength;
|
||||
|
||||
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
|
||||
|
||||
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
|
||||
@ -1779,6 +1853,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
_scrollIndex = config.scrollIndex;
|
||||
indexInParent = config.indexInParent;
|
||||
_platformViewId = config._platformViewId;
|
||||
_maxValueLength = config._maxValueLength;
|
||||
_currentValueLength = config._currentValueLength;
|
||||
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
||||
|
||||
assert(
|
||||
@ -1814,6 +1890,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
double scrollExtentMax = _scrollExtentMax;
|
||||
double scrollExtentMin = _scrollExtentMin;
|
||||
int platformViewId = _platformViewId;
|
||||
int maxValueLength = _maxValueLength;
|
||||
int currentValueLength = _currentValueLength;
|
||||
final double elevation = _elevation;
|
||||
double thickness = _thickness;
|
||||
final Set<int> customSemanticsActionIds = <int>{};
|
||||
@ -1849,6 +1927,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
scrollExtentMax ??= node._scrollExtentMax;
|
||||
scrollExtentMin ??= node._scrollExtentMin;
|
||||
platformViewId ??= node._platformViewId;
|
||||
maxValueLength ??= node._maxValueLength;
|
||||
currentValueLength ??= node._currentValueLength;
|
||||
if (value == '' || value == null)
|
||||
value = node._value;
|
||||
if (increasedValue == '' || increasedValue == null)
|
||||
@ -1919,6 +1999,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
scrollExtentMax: scrollExtentMax,
|
||||
scrollExtentMin: scrollExtentMin,
|
||||
platformViewId: platformViewId,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
|
||||
);
|
||||
}
|
||||
@ -1975,6 +2057,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
|
||||
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
|
||||
platformViewId: data.platformViewId ?? -1,
|
||||
maxValueLength: data.maxValueLength ?? -1,
|
||||
currentValueLength: data.currentValueLength ?? -1,
|
||||
scrollChildren: data.scrollChildCount ?? 0,
|
||||
scrollIndex: data.scrollIndex ?? 0 ,
|
||||
scrollPosition: data.scrollPosition ?? double.nan,
|
||||
@ -2118,6 +2202,8 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
|
||||
if (_textSelection?.isValid == true)
|
||||
properties.add(MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
|
||||
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
|
||||
properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null));
|
||||
properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null));
|
||||
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
|
||||
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
|
||||
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
|
||||
@ -3188,6 +3274,40 @@ class SemanticsConfiguration {
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// The maximum number of characters that can be entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [isTextField] is true. Defaults to null,
|
||||
/// which means no limit is imposed on the text field.
|
||||
int get maxValueLength => _maxValueLength;
|
||||
int _maxValueLength;
|
||||
set maxValueLength(int value) {
|
||||
if (value == maxValueLength)
|
||||
return;
|
||||
_maxValueLength = value;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// The current number of characters that have been entered into an editable
|
||||
/// text field.
|
||||
///
|
||||
/// For the purpose of this function a character is defined as one Unicode
|
||||
/// scalar value.
|
||||
///
|
||||
/// This should only be set when [isTextField] is true. Must be set when
|
||||
/// [maxValueLength] is set.
|
||||
int get currentValueLength => _currentValueLength;
|
||||
int _currentValueLength;
|
||||
set currentValueLength(int value) {
|
||||
if (value == currentValueLength)
|
||||
return;
|
||||
_currentValueLength = value;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// Whether the semantic information provided by the owning [RenderObject] and
|
||||
/// all of its descendants should be treated as one logical entity.
|
||||
///
|
||||
@ -3690,6 +3810,12 @@ class SemanticsConfiguration {
|
||||
if (_platformViewId != null && other._platformViewId != null) {
|
||||
return false;
|
||||
}
|
||||
if (_maxValueLength != null && other._maxValueLength != null) {
|
||||
return false;
|
||||
}
|
||||
if (_currentValueLength != null && other._currentValueLength != null) {
|
||||
return false;
|
||||
}
|
||||
if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty)
|
||||
return false;
|
||||
return true;
|
||||
@ -3725,6 +3851,8 @@ class SemanticsConfiguration {
|
||||
_scrollIndex ??= child._scrollIndex;
|
||||
_scrollChildCount ??= child._scrollChildCount;
|
||||
_platformViewId ??= child._platformViewId;
|
||||
_maxValueLength ??= child._maxValueLength;
|
||||
_currentValueLength ??= child._currentValueLength;
|
||||
|
||||
textDirection ??= child.textDirection;
|
||||
_sortKey ??= child._sortKey;
|
||||
@ -3781,6 +3909,8 @@ class SemanticsConfiguration {
|
||||
.._scrollIndex = _scrollIndex
|
||||
.._scrollChildCount = _scrollChildCount
|
||||
.._platformViewId = _platformViewId
|
||||
.._maxValueLength = _maxValueLength
|
||||
.._currentValueLength = _currentValueLength
|
||||
.._actions.addAll(_actions)
|
||||
.._customSemanticsActions.addAll(_customSemanticsActions);
|
||||
}
|
||||
|
@ -6196,6 +6196,8 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
bool hidden,
|
||||
bool image,
|
||||
bool liveRegion,
|
||||
int maxValueLength,
|
||||
int currentValueLength,
|
||||
String label,
|
||||
String value,
|
||||
String increasedValue,
|
||||
@ -6247,6 +6249,8 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
hidden: hidden,
|
||||
image: image,
|
||||
liveRegion: liveRegion,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
label: label,
|
||||
value: value,
|
||||
increasedValue: increasedValue,
|
||||
@ -6350,6 +6354,8 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
readOnly: properties.readOnly,
|
||||
focused: properties.focused,
|
||||
liveRegion: properties.liveRegion,
|
||||
maxValueLength: properties.maxValueLength,
|
||||
currentValueLength: properties.currentValueLength,
|
||||
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
|
||||
obscured: properties.obscured,
|
||||
multiline: properties.multiline,
|
||||
@ -6422,6 +6428,8 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
..hidden = properties.hidden
|
||||
..image = properties.image
|
||||
..liveRegion = properties.liveRegion
|
||||
..maxValueLength = properties.maxValueLength
|
||||
..currentValueLength = properties.currentValueLength
|
||||
..label = properties.label
|
||||
..value = properties.value
|
||||
..increasedValue = properties.increasedValue
|
||||
|
@ -3367,6 +3367,68 @@ void main() {
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Disabled text field does not have tap action', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
maxLength: 10,
|
||||
enabled: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('currentValueLength/maxValueLength are in the tree', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
maxLength: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
|
||||
maxValueLength: 10,
|
||||
currentValueLength: 0,
|
||||
));
|
||||
|
||||
await tester.showKeyboard(find.byType(TextField));
|
||||
const String testValue = '123';
|
||||
tester.testTextInput.updateEditingValue(const TextEditingValue(
|
||||
text: testValue,
|
||||
selection: TextSelection.collapsed(offset: 3),
|
||||
composing: TextRange(start: 0, end: testValue.length),
|
||||
));
|
||||
await tester.pump();
|
||||
|
||||
expect(semantics, includesNodeWith(
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isFocused],
|
||||
maxValueLength: 10,
|
||||
currentValueLength: 3,
|
||||
));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
|
@ -447,6 +447,8 @@ void main() {
|
||||
' textDirection: null\n'
|
||||
' sortKey: null\n'
|
||||
' platformViewId: null\n'
|
||||
' maxValueLength: null\n'
|
||||
' currentValueLength: null\n'
|
||||
' scrollChildren: null\n'
|
||||
' scrollIndex: null\n'
|
||||
' scrollExtentMin: null\n'
|
||||
@ -543,6 +545,8 @@ void main() {
|
||||
' textDirection: null\n'
|
||||
' sortKey: null\n'
|
||||
' platformViewId: null\n'
|
||||
' maxValueLength: null\n'
|
||||
' currentValueLength: null\n'
|
||||
' scrollChildren: null\n'
|
||||
' scrollIndex: null\n'
|
||||
' scrollExtentMin: null\n'
|
||||
|
@ -452,8 +452,11 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
final dynamic semanticsDebuggerPainter = _getSemanticsDebuggerPainter(debuggerKey: debugger, tester: tester);
|
||||
final RenderObject renderTextfield = tester.renderObject(find.descendant(of: find.byKey(textField), matching: find.byType(Semantics)).first);
|
||||
|
||||
expect(
|
||||
_getMessageShownInSemanticsDebugger(widgetKey: textField, debuggerKey: debugger, tester: tester),
|
||||
semanticsDebuggerPainter.getMessage(renderTextfield.debugSemantics),
|
||||
'textfield',
|
||||
);
|
||||
});
|
||||
@ -463,6 +466,14 @@ String _getMessageShownInSemanticsDebugger({
|
||||
@required Key widgetKey,
|
||||
@required Key debuggerKey,
|
||||
@required WidgetTester tester,
|
||||
}) {
|
||||
final dynamic semanticsDebuggerPainter = _getSemanticsDebuggerPainter(debuggerKey: debuggerKey, tester: tester);
|
||||
return semanticsDebuggerPainter.getMessage(tester.renderObject(find.byKey(widgetKey)).debugSemantics);
|
||||
}
|
||||
|
||||
dynamic _getSemanticsDebuggerPainter({
|
||||
@required Key debuggerKey,
|
||||
@required WidgetTester tester,
|
||||
}) {
|
||||
final CustomPaint customPaint = tester.widgetList(find.descendant(
|
||||
of: find.byKey(debuggerKey),
|
||||
@ -470,5 +481,5 @@ String _getMessageShownInSemanticsDebugger({
|
||||
)).first;
|
||||
final dynamic semanticsDebuggerPainter = customPaint.foregroundPainter;
|
||||
expect(semanticsDebuggerPainter.runtimeType.toString(), '_SemanticsDebuggerPainter');
|
||||
return semanticsDebuggerPainter.getMessage(tester.renderObject(find.byKey(widgetKey)).debugSemantics);
|
||||
return semanticsDebuggerPainter;
|
||||
}
|
||||
|
@ -442,6 +442,8 @@ class SemanticsTester {
|
||||
double scrollPosition,
|
||||
double scrollExtentMax,
|
||||
double scrollExtentMin,
|
||||
int currentValueLength,
|
||||
int maxValueLength,
|
||||
SemanticsNode ancestor,
|
||||
}) {
|
||||
bool checkNode(SemanticsNode node) {
|
||||
@ -471,6 +473,12 @@ class SemanticsTester {
|
||||
return false;
|
||||
if (scrollExtentMin != null && !nearEqual(node.scrollExtentMin, scrollExtentMin, 0.1))
|
||||
return false;
|
||||
if (currentValueLength != null && node.currentValueLength != currentValueLength) {
|
||||
return false;
|
||||
}
|
||||
if (maxValueLength != null && node.maxValueLength != maxValueLength) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -713,7 +721,19 @@ class _IncludesNodeWith extends Matcher {
|
||||
this.scrollPosition,
|
||||
this.scrollExtentMax,
|
||||
this.scrollExtentMin,
|
||||
}) : assert(label != null || value != null || actions != null || flags != null || scrollPosition != null || scrollExtentMax != null || scrollExtentMin != null);
|
||||
this.maxValueLength,
|
||||
this.currentValueLength,
|
||||
}) : assert(
|
||||
label != null ||
|
||||
value != null ||
|
||||
actions != null ||
|
||||
flags != null ||
|
||||
scrollPosition != null ||
|
||||
scrollExtentMax != null ||
|
||||
scrollExtentMin != null ||
|
||||
maxValueLength != null ||
|
||||
currentValueLength != null
|
||||
);
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
@ -724,6 +744,8 @@ class _IncludesNodeWith extends Matcher {
|
||||
final double scrollPosition;
|
||||
final double scrollExtentMax;
|
||||
final double scrollExtentMin;
|
||||
final int currentValueLength;
|
||||
final int maxValueLength;
|
||||
|
||||
@override
|
||||
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
|
||||
@ -737,6 +759,8 @@ class _IncludesNodeWith extends Matcher {
|
||||
scrollPosition: scrollPosition,
|
||||
scrollExtentMax: scrollExtentMax,
|
||||
scrollExtentMin: scrollExtentMin,
|
||||
currentValueLength: currentValueLength,
|
||||
maxValueLength: maxValueLength,
|
||||
).isNotEmpty;
|
||||
}
|
||||
|
||||
@ -761,6 +785,8 @@ class _IncludesNodeWith extends Matcher {
|
||||
if (scrollPosition != null) 'scrollPosition "$scrollPosition"',
|
||||
if (scrollExtentMax != null) 'scrollExtentMax "$scrollExtentMax"',
|
||||
if (scrollExtentMin != null) 'scrollExtentMin "$scrollExtentMin"',
|
||||
if (currentValueLength != null) 'currentValueLength "$currentValueLength"',
|
||||
if (maxValueLength != null) 'maxValueLength "$maxValueLength"',
|
||||
];
|
||||
return strings.join(', ');
|
||||
}
|
||||
@ -780,6 +806,8 @@ Matcher includesNodeWith({
|
||||
double scrollPosition,
|
||||
double scrollExtentMax,
|
||||
double scrollExtentMin,
|
||||
int maxValueLength,
|
||||
int currentValueLength,
|
||||
}) {
|
||||
return _IncludesNodeWith(
|
||||
label: label,
|
||||
@ -791,5 +819,7 @@ Matcher includesNodeWith({
|
||||
scrollPosition: scrollPosition,
|
||||
scrollExtentMax: scrollExtentMax,
|
||||
scrollExtentMin: scrollExtentMin,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
);
|
||||
}
|
||||
|
@ -317,6 +317,7 @@ class Descendant extends SerializableFinder {
|
||||
@required this.of,
|
||||
@required this.matching,
|
||||
this.matchRoot = false,
|
||||
this.firstMatchOnly = false,
|
||||
});
|
||||
|
||||
/// The finder specifying the widget of which the descendant is to be found.
|
||||
@ -328,6 +329,9 @@ class Descendant extends SerializableFinder {
|
||||
/// Whether the widget matching [of] will be considered for a match.
|
||||
final bool matchRoot;
|
||||
|
||||
/// If true then only the first descendant matching `matching` will be returned.
|
||||
final bool firstMatchOnly;
|
||||
|
||||
@override
|
||||
String get finderType => 'Descendant';
|
||||
|
||||
@ -338,6 +342,7 @@ class Descendant extends SerializableFinder {
|
||||
..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value)))
|
||||
..addAll(<String, String>{
|
||||
'matchRoot': matchRoot ? 'true' : 'false',
|
||||
'firstMatchOnly': firstMatchOnly ? 'true' : 'false',
|
||||
});
|
||||
}
|
||||
|
||||
@ -359,6 +364,7 @@ class Descendant extends SerializableFinder {
|
||||
of: SerializableFinder.deserialize(of),
|
||||
matching: SerializableFinder.deserialize(matching),
|
||||
matchRoot: other['matchRoot'] == 'true',
|
||||
firstMatchOnly: other['firstMatchOnly'] == 'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -374,6 +380,7 @@ class Ancestor extends SerializableFinder {
|
||||
@required this.of,
|
||||
@required this.matching,
|
||||
this.matchRoot = false,
|
||||
this.firstMatchOnly = false,
|
||||
});
|
||||
|
||||
/// The finder specifying the widget of which the ancestor is to be found.
|
||||
@ -385,6 +392,9 @@ class Ancestor extends SerializableFinder {
|
||||
/// Whether the widget matching [of] will be considered for a match.
|
||||
final bool matchRoot;
|
||||
|
||||
/// If true then only the first ancestor matching `matching` will be returned.
|
||||
final bool firstMatchOnly;
|
||||
|
||||
@override
|
||||
String get finderType => 'Ancestor';
|
||||
|
||||
@ -395,6 +405,7 @@ class Ancestor extends SerializableFinder {
|
||||
..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value)))
|
||||
..addAll(<String, String>{
|
||||
'matchRoot': matchRoot ? 'true' : 'false',
|
||||
'firstMatchOnly': firstMatchOnly ? 'true' : 'false',
|
||||
});
|
||||
}
|
||||
|
||||
@ -416,6 +427,7 @@ class Ancestor extends SerializableFinder {
|
||||
of: SerializableFinder.deserialize(of),
|
||||
matching: SerializableFinder.deserialize(matching),
|
||||
matchRoot: other['matchRoot'] == 'true',
|
||||
firstMatchOnly: other['firstMatchOnly'] == 'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1199,22 +1199,30 @@ class CommonFinders {
|
||||
///
|
||||
/// If the `matchRoot` argument is true then the widget specified by `of` will
|
||||
/// be considered for a match. The argument defaults to false.
|
||||
///
|
||||
/// If `firstMatchOnly` is true then only the first ancestor matching
|
||||
/// `matching` will be returned. Defaults to false.
|
||||
SerializableFinder ancestor({
|
||||
@required SerializableFinder of,
|
||||
@required SerializableFinder matching,
|
||||
bool matchRoot = false,
|
||||
}) => Ancestor(of: of, matching: matching, matchRoot: matchRoot);
|
||||
bool firstMatchOnly = false,
|
||||
}) => Ancestor(of: of, matching: matching, matchRoot: matchRoot, firstMatchOnly: firstMatchOnly);
|
||||
|
||||
/// Finds the widget that is an descendant of the `of` parameter and that
|
||||
/// matches the `matching` parameter.
|
||||
///
|
||||
/// If the `matchRoot` argument is true then the widget specified by `of` will
|
||||
/// be considered for a match. The argument defaults to false.
|
||||
///
|
||||
/// If `firstMatchOnly` is true then only the first descendant matching
|
||||
/// `matching` will be returned. Defaults to false.
|
||||
SerializableFinder descendant({
|
||||
@required SerializableFinder of,
|
||||
@required SerializableFinder matching,
|
||||
bool matchRoot = false,
|
||||
}) => Descendant(of: of, matching: matching, matchRoot: matchRoot);
|
||||
bool firstMatchOnly = false,
|
||||
}) => Descendant(of: of, matching: matching, matchRoot: matchRoot, firstMatchOnly: firstMatchOnly);
|
||||
}
|
||||
|
||||
/// An immutable 2D floating-point offset used by Flutter Driver.
|
||||
|
@ -335,19 +335,21 @@ class FlutterDriverExtension {
|
||||
}
|
||||
|
||||
Finder _createAncestorFinder(Ancestor arguments) {
|
||||
return find.ancestor(
|
||||
final Finder finder = find.ancestor(
|
||||
of: _createFinder(arguments.of),
|
||||
matching: _createFinder(arguments.matching),
|
||||
matchRoot: arguments.matchRoot,
|
||||
);
|
||||
return arguments.firstMatchOnly ? finder.first : finder;
|
||||
}
|
||||
|
||||
Finder _createDescendantFinder(Descendant arguments) {
|
||||
return find.descendant(
|
||||
final Finder finder = find.descendant(
|
||||
of: _createFinder(arguments.of),
|
||||
matching: _createFinder(arguments.matching),
|
||||
matchRoot: arguments.matchRoot,
|
||||
);
|
||||
return arguments.firstMatchOnly ? finder.first : finder;
|
||||
}
|
||||
|
||||
Finder _createFinder(SerializableFinder finder) {
|
||||
|
@ -573,6 +573,39 @@ void main() {
|
||||
expect(await result, null);
|
||||
});
|
||||
|
||||
testWidgets('descendant finder firstMatchOnly', (WidgetTester tester) async {
|
||||
flutterDriverLog.listen((LogRecord _) {}); // Silence logging.
|
||||
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||
|
||||
Future<String> getDescendantText() async {
|
||||
final Map<String, Object> arguments = GetText(Descendant(
|
||||
of: ByValueKey('column'),
|
||||
matching: const ByType('Text'),
|
||||
firstMatchOnly: true,
|
||||
), timeout: const Duration(seconds: 1)).serialize();
|
||||
final Map<String, dynamic> result = await extension.call(arguments);
|
||||
if (result['isError']) {
|
||||
return null;
|
||||
}
|
||||
return GetTextResult.fromJson(result['response']).text;
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Column(
|
||||
key: const ValueKey<String>('column'),
|
||||
children: const <Widget>[
|
||||
Text('Hello1', key: ValueKey<String>('text1')),
|
||||
Text('Hello2', key: ValueKey<String>('text2')),
|
||||
Text('Hello3', key: ValueKey<String>('text3')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(await getDescendantText(), 'Hello1');
|
||||
});
|
||||
|
||||
testWidgets('ancestor finder', (WidgetTester tester) async {
|
||||
flutterDriverLog.listen((LogRecord _) {}); // Silence logging.
|
||||
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||
@ -642,6 +675,54 @@ void main() {
|
||||
expect(await result, null);
|
||||
});
|
||||
|
||||
testWidgets('ancestor finder firstMatchOnly', (WidgetTester tester) async {
|
||||
flutterDriverLog.listen((LogRecord _) {}); // Silence logging.
|
||||
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||
|
||||
Future<Offset> getAncestorTopLeft() async {
|
||||
final Map<String, Object> arguments = GetOffset(Ancestor(
|
||||
of: ByValueKey('leaf'),
|
||||
matching: const ByType('Container'),
|
||||
firstMatchOnly: true,
|
||||
), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize();
|
||||
final Map<String, dynamic> response = await extension.call(arguments);
|
||||
if (response['isError']) {
|
||||
return null;
|
||||
}
|
||||
final GetOffsetResult result = GetOffsetResult.fromJson(response['response']);
|
||||
return Offset(result.dx, result.dy);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: Container(
|
||||
height: 200,
|
||||
width: 200,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: Container(
|
||||
key: const ValueKey<String>('leaf'),
|
||||
height: 50,
|
||||
width: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await getAncestorTopLeft(),
|
||||
const Offset((800 - 100) / 2, (600 - 100) / 2),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('GetDiagnosticsTree', (WidgetTester tester) async {
|
||||
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||
|
||||
|
@ -15,6 +15,7 @@ void main() {
|
||||
of: of,
|
||||
matching: matching,
|
||||
matchRoot: true,
|
||||
firstMatchOnly: true,
|
||||
);
|
||||
expect(a.serialize(), <String, String>{
|
||||
'finderType': 'Ancestor',
|
||||
@ -23,7 +24,8 @@ void main() {
|
||||
'matching_finderType': 'ByValueKey',
|
||||
'matching_keyValueString': 'hello',
|
||||
'matching_keyValueType': 'String',
|
||||
'matchRoot': 'true'
|
||||
'matchRoot': 'true',
|
||||
'firstMatchOnly': 'true',
|
||||
});
|
||||
});
|
||||
|
||||
@ -35,13 +37,15 @@ void main() {
|
||||
'matching_finderType': 'ByValueKey',
|
||||
'matching_keyValueString': 'hello',
|
||||
'matching_keyValueType': 'String',
|
||||
'matchRoot': 'true'
|
||||
'matchRoot': 'true',
|
||||
'firstMatchOnly': 'true',
|
||||
};
|
||||
|
||||
final Ancestor a = Ancestor.deserialize(serialized);
|
||||
expect(a.of, isA<ByType>());
|
||||
expect(a.matching, isA<ByValueKey>());
|
||||
expect(a.matchRoot, isTrue);
|
||||
expect(a.firstMatchOnly, isTrue);
|
||||
});
|
||||
|
||||
test('Descendant finder serialize', () {
|
||||
@ -52,6 +56,7 @@ void main() {
|
||||
of: of,
|
||||
matching: matching,
|
||||
matchRoot: true,
|
||||
firstMatchOnly: true,
|
||||
);
|
||||
expect(a.serialize(), <String, String>{
|
||||
'finderType': 'Descendant',
|
||||
@ -60,7 +65,8 @@ void main() {
|
||||
'matching_finderType': 'ByValueKey',
|
||||
'matching_keyValueString': 'hello',
|
||||
'matching_keyValueType': 'String',
|
||||
'matchRoot': 'true'
|
||||
'matchRoot': 'true',
|
||||
'firstMatchOnly': 'true',
|
||||
});
|
||||
});
|
||||
|
||||
@ -72,12 +78,14 @@ void main() {
|
||||
'matching_finderType': 'ByValueKey',
|
||||
'matching_keyValueString': 'hello',
|
||||
'matching_keyValueType': 'String',
|
||||
'matchRoot': 'true'
|
||||
'matchRoot': 'true',
|
||||
'firstMatchOnly': 'true',
|
||||
};
|
||||
|
||||
final Descendant a = Descendant.deserialize(serialized);
|
||||
expect(a.of, isA<ByType>());
|
||||
expect(a.matching, isA<ByValueKey>());
|
||||
expect(a.matchRoot, isTrue);
|
||||
expect(a.firstMatchOnly, isTrue);
|
||||
});
|
||||
}
|
||||
|
@ -433,6 +433,8 @@ Matcher matchesSemantics({
|
||||
double elevation,
|
||||
double thickness,
|
||||
int platformViewId,
|
||||
int maxValueLength,
|
||||
int currentValueLength,
|
||||
// Flags //
|
||||
bool hasCheckedState = false,
|
||||
bool isChecked = false,
|
||||
@ -552,6 +554,8 @@ Matcher matchesSemantics({
|
||||
platformViewId: platformViewId,
|
||||
customActions: customActions,
|
||||
hintOverrides: hintOverrides,
|
||||
currentValueLength: currentValueLength,
|
||||
maxValueLength: maxValueLength,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
@ -1745,6 +1749,8 @@ class _MatchesSemanticsData extends Matcher {
|
||||
this.elevation,
|
||||
this.thickness,
|
||||
this.platformViewId,
|
||||
this.maxValueLength,
|
||||
this.currentValueLength,
|
||||
this.customActions,
|
||||
this.hintOverrides,
|
||||
this.children,
|
||||
@ -1765,6 +1771,8 @@ class _MatchesSemanticsData extends Matcher {
|
||||
final double elevation;
|
||||
final double thickness;
|
||||
final int platformViewId;
|
||||
final int maxValueLength;
|
||||
final int currentValueLength;
|
||||
final List<Matcher> children;
|
||||
|
||||
@override
|
||||
@ -1796,6 +1804,10 @@ class _MatchesSemanticsData extends Matcher {
|
||||
description.add(' with thickness: $thickness');
|
||||
if (platformViewId != null)
|
||||
description.add(' with platformViewId: $platformViewId');
|
||||
if (maxValueLength != null)
|
||||
description.add(' with maxValueLength: $maxValueLength');
|
||||
if (currentValueLength != null)
|
||||
description.add(' with currentValueLength: $currentValueLength');
|
||||
if (customActions != null)
|
||||
description.add(' with custom actions: $customActions');
|
||||
if (hintOverrides != null)
|
||||
@ -1838,6 +1850,10 @@ class _MatchesSemanticsData extends Matcher {
|
||||
return failWithDescription(matchState, 'thickness was: ${data.thickness}');
|
||||
if (platformViewId != null && platformViewId != data.platformViewId)
|
||||
return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
|
||||
if (currentValueLength != null && currentValueLength != data.currentValueLength)
|
||||
return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}');
|
||||
if (maxValueLength != null && maxValueLength != data.maxValueLength)
|
||||
return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
|
||||
if (actions != null) {
|
||||
int actionBits = 0;
|
||||
for (SemanticsAction action in actions)
|
||||
|
@ -525,6 +525,8 @@ void main() {
|
||||
scrollExtentMin: null,
|
||||
platformViewId: 105,
|
||||
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
|
||||
currentValueLength: 10,
|
||||
maxValueLength: 15,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode();
|
||||
node.data = data;
|
||||
@ -535,6 +537,8 @@ void main() {
|
||||
elevation: 3.0,
|
||||
thickness: 4.0,
|
||||
platformViewId: 105,
|
||||
currentValueLength: 10,
|
||||
maxValueLength: 15,
|
||||
/* Flags */
|
||||
hasCheckedState: true,
|
||||
isChecked: true,
|
||||
|
Loading…
Reference in New Issue
Block a user