mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
Add ancestor and descendant finders to Driver (#32410)
This commit is contained in:
parent
705143855f
commit
b37c3be0fa
|
@ -148,6 +148,8 @@ abstract class SerializableFinder {
|
||||||
case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
|
case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
|
||||||
case 'ByText': return ByText.deserialize(json);
|
case 'ByText': return ByText.deserialize(json);
|
||||||
case 'PageBack': return const PageBack();
|
case 'PageBack': return const PageBack();
|
||||||
|
case 'Descendant': return Descendant.deserialize(json);
|
||||||
|
case 'Ancestor': return Ancestor.deserialize(json);
|
||||||
}
|
}
|
||||||
throw DriverError('Unsupported search specification type $finderType');
|
throw DriverError('Unsupported search specification type $finderType');
|
||||||
}
|
}
|
||||||
|
@ -317,6 +319,120 @@ class PageBack extends SerializableFinder {
|
||||||
String get finderType => 'PageBack';
|
String get finderType => 'PageBack';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A Flutter Driver finder that finds a descendant of [of] that matches
|
||||||
|
/// [matching].
|
||||||
|
///
|
||||||
|
/// If the `matchRoot` argument is true, then the widget specified by [of] will
|
||||||
|
/// be considered for a match. The argument defaults to false.
|
||||||
|
class Descendant extends SerializableFinder {
|
||||||
|
/// Creates a descendant finder.
|
||||||
|
const Descendant({
|
||||||
|
@required this.of,
|
||||||
|
@required this.matching,
|
||||||
|
this.matchRoot = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The finder specifying the widget of which the descendant is to be found.
|
||||||
|
final SerializableFinder of;
|
||||||
|
|
||||||
|
/// Only a descendant of [of] matching this finder will be found.
|
||||||
|
final SerializableFinder matching;
|
||||||
|
|
||||||
|
/// Whether the widget matching [of] will be considered for a match.
|
||||||
|
final bool matchRoot;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get finderType => 'Descendant';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> serialize() {
|
||||||
|
return super.serialize()
|
||||||
|
..addAll(of.serialize().map((String key, String value) => MapEntry<String, String>('of_$key', value)))
|
||||||
|
..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value)))
|
||||||
|
..addAll(<String, String>{
|
||||||
|
'matchRoot': matchRoot ? 'true' : 'false',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes the finder from JSON generated by [serialize].
|
||||||
|
static Descendant deserialize(Map<String, String> json) {
|
||||||
|
final Map<String, String> of = <String, String>{};
|
||||||
|
final Map<String, String> matching = <String, String>{};
|
||||||
|
final Map<String, String> other = <String, String>{};
|
||||||
|
for (String key in json.keys) {
|
||||||
|
if (key.startsWith('of_')) {
|
||||||
|
of[key.substring('of_'.length)] = json[key];
|
||||||
|
} else if (key.startsWith('matching_')) {
|
||||||
|
matching[key.substring('matching_'.length)] = json[key];
|
||||||
|
} else {
|
||||||
|
other[key] = json[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Descendant(
|
||||||
|
of: SerializableFinder.deserialize(of),
|
||||||
|
matching: SerializableFinder.deserialize(matching),
|
||||||
|
matchRoot: other['matchRoot'] == 'true',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Flutter Driver finder that finds an ancestor of [of] that matches
|
||||||
|
/// [matching].
|
||||||
|
///
|
||||||
|
/// If the `matchRoot` argument is true, then the widget specified by [of] will
|
||||||
|
/// be considered for a match. The argument defaults to false.
|
||||||
|
class Ancestor extends SerializableFinder {
|
||||||
|
/// Creates an ancestor finder.
|
||||||
|
const Ancestor({
|
||||||
|
@required this.of,
|
||||||
|
@required this.matching,
|
||||||
|
this.matchRoot = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The finder specifying the widget of which the ancestor is to be found.
|
||||||
|
final SerializableFinder of;
|
||||||
|
|
||||||
|
/// Only an ancestor of [of] matching this finder will be found.
|
||||||
|
final SerializableFinder matching;
|
||||||
|
|
||||||
|
/// Whether the widget matching [of] will be considered for a match.
|
||||||
|
final bool matchRoot;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get finderType => 'Ancestor';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, String> serialize() {
|
||||||
|
return super.serialize()
|
||||||
|
..addAll(of.serialize().map((String key, String value) => MapEntry<String, String>('of_$key', value)))
|
||||||
|
..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value)))
|
||||||
|
..addAll(<String, String>{
|
||||||
|
'matchRoot': matchRoot ? 'true' : 'false',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes the finder from JSON generated by [serialize].
|
||||||
|
static Ancestor deserialize(Map<String, String> json) {
|
||||||
|
final Map<String, String> of = <String, String>{};
|
||||||
|
final Map<String, String> matching = <String, String>{};
|
||||||
|
final Map<String, String> other = <String, String>{};
|
||||||
|
for (String key in json.keys) {
|
||||||
|
if (key.startsWith('of_')) {
|
||||||
|
of[key.substring('of_'.length)] = json[key];
|
||||||
|
} else if (key.startsWith('matching_')) {
|
||||||
|
matching[key.substring('matching_'.length)] = json[key];
|
||||||
|
} else {
|
||||||
|
other[key] = json[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ancestor(
|
||||||
|
of: SerializableFinder.deserialize(of),
|
||||||
|
matching: SerializableFinder.deserialize(matching),
|
||||||
|
matchRoot: other['matchRoot'] == 'true',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A Flutter driver command that retrieves a semantics id using a specified finder.
|
/// A Flutter driver command that retrieves a semantics id using a specified finder.
|
||||||
///
|
///
|
||||||
/// This command requires assertions to be enabled on the device.
|
/// This command requires assertions to be enabled on the device.
|
||||||
|
|
|
@ -1017,6 +1017,28 @@ class CommonFinders {
|
||||||
|
|
||||||
/// Finds the back button on a Material or Cupertino page's scaffold.
|
/// Finds the back button on a Material or Cupertino page's scaffold.
|
||||||
SerializableFinder pageBack() => const PageBack();
|
SerializableFinder pageBack() => const PageBack();
|
||||||
|
|
||||||
|
/// Finds the widget that is an ancestor 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.
|
||||||
|
SerializableFinder ancestor({
|
||||||
|
@required SerializableFinder of,
|
||||||
|
@required SerializableFinder matching,
|
||||||
|
bool matchRoot = false,
|
||||||
|
}) => Ancestor(of: of, matching: matching, matchRoot: matchRoot);
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
SerializableFinder descendant({
|
||||||
|
@required SerializableFinder of,
|
||||||
|
@required SerializableFinder matching,
|
||||||
|
bool matchRoot = false,
|
||||||
|
}) => Descendant(of: of, matching: matching, matchRoot: matchRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An immutable 2D floating-point offset used by Flutter Driver.
|
/// An immutable 2D floating-point offset used by Flutter Driver.
|
||||||
|
|
|
@ -142,6 +142,8 @@ class FlutterDriverExtension {
|
||||||
'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder),
|
'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder),
|
||||||
'ByType': (SerializableFinder finder) => _createByTypeFinder(finder),
|
'ByType': (SerializableFinder finder) => _createByTypeFinder(finder),
|
||||||
'PageBack': (SerializableFinder finder) => _createPageBackFinder(),
|
'PageBack': (SerializableFinder finder) => _createPageBackFinder(),
|
||||||
|
'Ancestor': (SerializableFinder finder) => _createAncestorFinder(finder),
|
||||||
|
'Descendant': (SerializableFinder finder) => _createDescendantFinder(finder),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,6 +312,22 @@ class FlutterDriverExtension {
|
||||||
}, description: 'Material or Cupertino back button');
|
}, description: 'Material or Cupertino back button');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Finder _createAncestorFinder(Ancestor arguments) {
|
||||||
|
return find.ancestor(
|
||||||
|
of: _createFinder(arguments.of),
|
||||||
|
matching: _createFinder(arguments.matching),
|
||||||
|
matchRoot: arguments.matchRoot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder _createDescendantFinder(Descendant arguments) {
|
||||||
|
return find.descendant(
|
||||||
|
of: _createFinder(arguments.of),
|
||||||
|
matching: _createFinder(arguments.matching),
|
||||||
|
matchRoot: arguments.matchRoot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Finder _createFinder(SerializableFinder finder) {
|
Finder _createFinder(SerializableFinder finder) {
|
||||||
final FinderConstructor constructor = _finders[finder.finderType];
|
final FinderConstructor constructor = _finders[finder.finderType];
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_driver/flutter_driver.dart';
|
||||||
import 'package:flutter_driver/src/common/find.dart';
|
import 'package:flutter_driver/src/common/find.dart';
|
||||||
import 'package:flutter_driver/src/common/geometry.dart';
|
import 'package:flutter_driver/src/common/geometry.dart';
|
||||||
import 'package:flutter_driver/src/common/request_data.dart';
|
import 'package:flutter_driver/src/common/request_data.dart';
|
||||||
|
import 'package:flutter_driver/src/common/text.dart';
|
||||||
import 'package:flutter_driver/src/extension/extension.dart';
|
import 'package:flutter_driver/src/extension/extension.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
@ -151,4 +154,117 @@ void main() {
|
||||||
expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0));
|
expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0));
|
||||||
expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2)));
|
expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('descendant finder', (WidgetTester tester) async {
|
||||||
|
flutterDriverLog.listen((LogRecord _) {}); // Silence logging.
|
||||||
|
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||||
|
|
||||||
|
Future<String> getDescendantText({ String of, bool matchRoot = false}) async {
|
||||||
|
final Map<String, Object> arguments = GetText(Descendant(
|
||||||
|
of: ByValueKey(of),
|
||||||
|
matching: ByValueKey('text2'),
|
||||||
|
matchRoot: matchRoot,
|
||||||
|
), 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(of: 'column'), 'Hello2');
|
||||||
|
expect(await getDescendantText(of: 'column', matchRoot: true), 'Hello2');
|
||||||
|
expect(await getDescendantText(of: 'text2', matchRoot: true), 'Hello2');
|
||||||
|
|
||||||
|
// Find nothing
|
||||||
|
Future<String> result = getDescendantText(of: 'text1', matchRoot: true);
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
expect(await result, null);
|
||||||
|
|
||||||
|
result = getDescendantText(of: 'text2');
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
expect(await result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ancestor finder', (WidgetTester tester) async {
|
||||||
|
flutterDriverLog.listen((LogRecord _) {}); // Silence logging.
|
||||||
|
final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true);
|
||||||
|
|
||||||
|
Future<Offset> getAncestorTopLeft({ String of, String matching, bool matchRoot = false}) async {
|
||||||
|
final Map<String, Object> arguments = GetOffset(Ancestor(
|
||||||
|
of: ByValueKey(of),
|
||||||
|
matching: ByValueKey(matching),
|
||||||
|
matchRoot: matchRoot,
|
||||||
|
), 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(
|
||||||
|
key: const ValueKey<String>('parent'),
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
key: const ValueKey<String>('leftchild'),
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
key: const ValueKey<String>('righttchild'),
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await getAncestorTopLeft(of: 'leftchild', matching: 'parent'),
|
||||||
|
const Offset((800 - 100) / 2, (600 - 100) / 2),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await getAncestorTopLeft(of: 'leftchild', matching: 'parent', matchRoot: true),
|
||||||
|
const Offset((800 - 100) / 2, (600 - 100) / 2),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await getAncestorTopLeft(of: 'parent', matching: 'parent', matchRoot: true),
|
||||||
|
const Offset((800 - 100) / 2, (600 - 100) / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find nothing
|
||||||
|
Future<Offset> result = getAncestorTopLeft(of: 'leftchild', matching: 'leftchild');
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
expect(await result, null);
|
||||||
|
|
||||||
|
result = getAncestorTopLeft(of: 'leftchild', matching: 'righttchild');
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
expect(await result, null);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
83
packages/flutter_driver/test/src/find_test.dart
Normal file
83
packages/flutter_driver/test/src/find_test.dart
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// Copyright 2019 The Chromium 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:flutter_driver/src/common/find.dart';
|
||||||
|
|
||||||
|
import '../common.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Ancestor finder serialize', () {
|
||||||
|
const SerializableFinder of = ByType('Text');
|
||||||
|
final SerializableFinder matching = ByValueKey('hello');
|
||||||
|
|
||||||
|
final Ancestor a = Ancestor(
|
||||||
|
of: of,
|
||||||
|
matching: matching,
|
||||||
|
matchRoot: true,
|
||||||
|
);
|
||||||
|
expect(a.serialize(), <String, String>{
|
||||||
|
'finderType': 'Ancestor',
|
||||||
|
'of_finderType': 'ByType',
|
||||||
|
'of_type': 'Text',
|
||||||
|
'matching_finderType': 'ByValueKey',
|
||||||
|
'matching_keyValueString': 'hello',
|
||||||
|
'matching_keyValueType': 'String',
|
||||||
|
'matchRoot': 'true'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Ancestor finder deserialize', () {
|
||||||
|
final Map<String, String> serialized = <String, String>{
|
||||||
|
'finderType': 'Ancestor',
|
||||||
|
'of_finderType': 'ByType',
|
||||||
|
'of_type': 'Text',
|
||||||
|
'matching_finderType': 'ByValueKey',
|
||||||
|
'matching_keyValueString': 'hello',
|
||||||
|
'matching_keyValueType': 'String',
|
||||||
|
'matchRoot': 'true'
|
||||||
|
};
|
||||||
|
|
||||||
|
final Ancestor a = Ancestor.deserialize(serialized);
|
||||||
|
expect(a.of, isA<ByType>());
|
||||||
|
expect(a.matching, isA<ByValueKey>());
|
||||||
|
expect(a.matchRoot, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Descendant finder serialize', () {
|
||||||
|
const SerializableFinder of = ByType('Text');
|
||||||
|
final SerializableFinder matching = ByValueKey('hello');
|
||||||
|
|
||||||
|
final Descendant a = Descendant(
|
||||||
|
of: of,
|
||||||
|
matching: matching,
|
||||||
|
matchRoot: true,
|
||||||
|
);
|
||||||
|
expect(a.serialize(), <String, String>{
|
||||||
|
'finderType': 'Descendant',
|
||||||
|
'of_finderType': 'ByType',
|
||||||
|
'of_type': 'Text',
|
||||||
|
'matching_finderType': 'ByValueKey',
|
||||||
|
'matching_keyValueString': 'hello',
|
||||||
|
'matching_keyValueType': 'String',
|
||||||
|
'matchRoot': 'true'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Descendant finder deserialize', () {
|
||||||
|
final Map<String, String> serialized = <String, String>{
|
||||||
|
'finderType': 'Descendant',
|
||||||
|
'of_finderType': 'ByType',
|
||||||
|
'of_type': 'Text',
|
||||||
|
'matching_finderType': 'ByValueKey',
|
||||||
|
'matching_keyValueString': 'hello',
|
||||||
|
'matching_keyValueType': 'String',
|
||||||
|
'matchRoot': 'true'
|
||||||
|
};
|
||||||
|
|
||||||
|
final Descendant a = Descendant.deserialize(serialized);
|
||||||
|
expect(a.of, isA<ByType>());
|
||||||
|
expect(a.matching, isA<ByValueKey>());
|
||||||
|
expect(a.matchRoot, isTrue);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue