mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
parent
79e3bf4ad3
commit
5e27ebbe8d
@ -135,6 +135,7 @@ abstract class SerializableFinder {
|
||||
case 'ByType': return ByType.deserialize(json);
|
||||
case 'ByValueKey': return ByValueKey.deserialize(json);
|
||||
case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
|
||||
case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
|
||||
case 'ByText': return ByText.deserialize(json);
|
||||
case 'PageBack': return PageBack();
|
||||
}
|
||||
@ -164,6 +165,44 @@ class ByTooltipMessage extends SerializableFinder {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Flutter Driver finder that finds widgets by semantic label.
|
||||
///
|
||||
/// If the [label] property is a [String], the finder will try to find an exact
|
||||
/// match. If it is a [RegExp], it will return true for [RegExp.hasMatch].
|
||||
class BySemanticsLabel extends SerializableFinder {
|
||||
/// Creates a semantic label finder given the [label].
|
||||
BySemanticsLabel(this.label);
|
||||
|
||||
/// A [Pattern] matching the [Semantics.properties.label].
|
||||
///
|
||||
/// If this is a [String], it will be treated as an exact match.
|
||||
final Pattern label;
|
||||
|
||||
@override
|
||||
final String finderType = 'BySemanticsLabel';
|
||||
|
||||
@override
|
||||
Map<String, String> serialize() {
|
||||
if (label is RegExp) {
|
||||
final RegExp regExp = label;
|
||||
return super.serialize()..addAll(<String, String>{
|
||||
'label': regExp.pattern,
|
||||
'isRegExp': 'true',
|
||||
});
|
||||
} else {
|
||||
return super.serialize()..addAll(<String, String>{
|
||||
'label': label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes the finder from JSON generated by [serialize].
|
||||
static BySemanticsLabel deserialize(Map<String, String> json) {
|
||||
final bool isRegExp = json['isRegExp'] == 'true';
|
||||
return BySemanticsLabel(isRegExp ? RegExp(json['label']) : json['label']);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Flutter Driver finder that finds widgets by [text] inside a [Text] or
|
||||
/// [EditableText] widget.
|
||||
class ByText extends SerializableFinder {
|
||||
|
||||
@ -939,6 +939,9 @@ class CommonFinders {
|
||||
/// Finds widgets with a tooltip with the given [message].
|
||||
SerializableFinder byTooltip(String message) => ByTooltipMessage(message);
|
||||
|
||||
/// Finds widgets with the given semantics [label].
|
||||
SerializableFinder bySemanticsLabel(Pattern label) => BySemanticsLabel(label);
|
||||
|
||||
/// Finds widgets whose class name matches the given string.
|
||||
SerializableFinder byType(String type) => ByType(type);
|
||||
|
||||
|
||||
@ -135,6 +135,7 @@ class FlutterDriverExtension {
|
||||
_finders.addAll(<String, FinderConstructor>{
|
||||
'ByText': (SerializableFinder finder) => _createByTextFinder(finder),
|
||||
'ByTooltipMessage': (SerializableFinder finder) => _createByTooltipMessageFinder(finder),
|
||||
'BySemanticsLabel': (SerializableFinder finder) => _createBySemanticsLabelFinder(finder),
|
||||
'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder),
|
||||
'ByType': (SerializableFinder finder) => _createByTypeFinder(finder),
|
||||
'PageBack': (SerializableFinder finder) => _createPageBackFinder(),
|
||||
@ -262,6 +263,22 @@ class FlutterDriverExtension {
|
||||
}, description: 'widget with text tooltip "${arguments.text}"');
|
||||
}
|
||||
|
||||
Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) {
|
||||
return find.byElementPredicate((Element element) {
|
||||
if (element is! RenderObjectElement) {
|
||||
return false;
|
||||
}
|
||||
final String semanticsLabel = element.renderObject?.debugSemantics?.label;
|
||||
if (semanticsLabel == null) {
|
||||
return false;
|
||||
}
|
||||
final Pattern label = arguments.label;
|
||||
return label is RegExp
|
||||
? label.hasMatch(semanticsLabel)
|
||||
: label == semanticsLabel;
|
||||
}, description: 'widget with semantic label "${arguments.label}"');
|
||||
}
|
||||
|
||||
Finder _createByValueKeyFinder(ByValueKey arguments) {
|
||||
switch (arguments.keyValueType) {
|
||||
case 'int':
|
||||
|
||||
@ -158,6 +158,35 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('BySemanticsLabel', () {
|
||||
test('finds by Semantic label using String', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||
expect(i.positionalArguments[1], <String, String>{
|
||||
'command': 'tap',
|
||||
'timeout': _kSerializedTestTimeout,
|
||||
'finderType': 'BySemanticsLabel',
|
||||
'label': 'foo',
|
||||
});
|
||||
return makeMockResponse(<String, dynamic>{});
|
||||
});
|
||||
await driver.tap(find.bySemanticsLabel('foo'), timeout: _kTestTimeout);
|
||||
});
|
||||
|
||||
test('finds by Semantic label using RegExp', () async {
|
||||
when(mockIsolate.invokeExtension(any, any)).thenAnswer((Invocation i) {
|
||||
expect(i.positionalArguments[1], <String, String>{
|
||||
'command': 'tap',
|
||||
'timeout': _kSerializedTestTimeout,
|
||||
'finderType': 'BySemanticsLabel',
|
||||
'label': '^foo',
|
||||
'isRegExp': 'true',
|
||||
});
|
||||
return makeMockResponse(<String, dynamic>{});
|
||||
});
|
||||
await driver.tap(find.bySemanticsLabel(RegExp('^foo')), timeout: _kTestTimeout);
|
||||
});
|
||||
});
|
||||
|
||||
group('tap', () {
|
||||
test('requires a target reference', () async {
|
||||
expect(driver.tap(null), throwsA(isInstanceOf<DriverError>()));
|
||||
|
||||
@ -272,6 +272,51 @@ class CommonFinders {
|
||||
Finder ancestor({ Finder of, Finder matching, bool matchRoot = false }) {
|
||||
return _AncestorFinder(of, matching, matchRoot: matchRoot);
|
||||
}
|
||||
|
||||
/// Finds [Semantics] widgets matching the given `label`, either by
|
||||
/// [RegExp.hasMatch] or string equality.
|
||||
///
|
||||
/// The framework may combine semantics labels in certain scenarios, such as
|
||||
/// when multiple [Text] widgets are in a [MaterialButton] widget. In such a
|
||||
/// case, it may be preferable to match by regular expression. Consumers of
|
||||
/// this API __must not__ introduce unsuitable content into the semantics tree
|
||||
/// for the purposes of testing; in particular, you should prefer matching by
|
||||
/// regular expression rather than by string if the framework has combined
|
||||
/// your semantics, and not try to force the framework to break up the
|
||||
/// semantics nodes. Breaking up the nodes would have an undesirable effect on
|
||||
/// screen readers and other accessibility services.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.BySemanticsLabel('Back'), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) {
|
||||
if (WidgetsBinding.instance.pipelineOwner.semanticsOwner == null)
|
||||
throw StateError('Semantics are not enabled. '
|
||||
'Make sure to call tester.enableSemantics() before using '
|
||||
'this finder, and call dispose on its return value after.');
|
||||
return byElementPredicate(
|
||||
(Element element) {
|
||||
// Multiple elements can have the same renderObject - we want the "owner"
|
||||
// of the renderObject, i.e. the RenderObjectElement.
|
||||
if (element is! RenderObjectElement) {
|
||||
return false;
|
||||
}
|
||||
final String semanticsLabel = element.renderObject?.debugSemantics?.label;
|
||||
if (semanticsLabel == null) {
|
||||
return false;
|
||||
}
|
||||
return label is RegExp
|
||||
? label.hasMatch(semanticsLabel)
|
||||
: label == semanticsLabel;
|
||||
},
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches a widget tree and returns nodes that match a particular
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -29,6 +30,53 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('semantics', () {
|
||||
testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async {
|
||||
expect(() => find.bySemanticsLabel('Add'), throwsStateError);
|
||||
});
|
||||
|
||||
testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
await tester.pumpWidget(_boilerplate(
|
||||
Semantics(
|
||||
label: 'Add',
|
||||
button: true,
|
||||
child: const FlatButton(
|
||||
child: Text('+'),
|
||||
onPressed: null,
|
||||
),
|
||||
),
|
||||
));
|
||||
expect(find.bySemanticsLabel('Add'), findsOneWidget);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('finds Semantically labeled widgets by RegExp', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
await tester.pumpWidget(_boilerplate(
|
||||
Semantics(
|
||||
container: true,
|
||||
child: Row(children: const <Widget>[
|
||||
Text('Hello'),
|
||||
Text('World'),
|
||||
]),
|
||||
),
|
||||
));
|
||||
expect(find.bySemanticsLabel('Hello'), findsNothing);
|
||||
expect(find.bySemanticsLabel(RegExp(r'^Hello')), findsOneWidget);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('finds Semantically labeled widgets without explicit Semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
await tester.pumpWidget(_boilerplate(
|
||||
const SimpleCustomSemanticsWidget('Foo')
|
||||
));
|
||||
expect(find.bySemanticsLabel('Foo'), findsOneWidget);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
group('hitTestable', () {
|
||||
testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
@ -92,3 +140,28 @@ Widget _boilerplate(Widget child) {
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget {
|
||||
const SimpleCustomSemanticsWidget(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) => SimpleCustomSemanticsRenderObject(label);
|
||||
}
|
||||
|
||||
class SimpleCustomSemanticsRenderObject extends RenderBox {
|
||||
SimpleCustomSemanticsRenderObject(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
config..label = label..textDirection = TextDirection.ltr;
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user