Add ancestor and descendant finders to Driver (#32410)

This commit is contained in:
Michael Goderbauer 2019-05-10 18:21:19 +02:00 committed by GitHub
parent 705143855f
commit b37c3be0fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 355 additions and 0 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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];

View file

@ -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);
});
} }

View 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);
});
}