diff --git a/packages/flutter_driver/lib/src/common/find.dart b/packages/flutter_driver/lib/src/common/find.dart index 3423876df6..4eb6576271 100644 --- a/packages/flutter_driver/lib/src/common/find.dart +++ b/packages/flutter_driver/lib/src/common/find.dart @@ -148,6 +148,8 @@ abstract class SerializableFinder { case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json); case 'ByText': return ByText.deserialize(json); case 'PageBack': return const PageBack(); + case 'Descendant': return Descendant.deserialize(json); + case 'Ancestor': return Ancestor.deserialize(json); } throw DriverError('Unsupported search specification type $finderType'); } @@ -317,6 +319,120 @@ class PageBack extends SerializableFinder { 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 serialize() { + return super.serialize() + ..addAll(of.serialize().map((String key, String value) => MapEntry('of_$key', value))) + ..addAll(matching.serialize().map((String key, String value) => MapEntry('matching_$key', value))) + ..addAll({ + 'matchRoot': matchRoot ? 'true' : 'false', + }); + } + + /// Deserializes the finder from JSON generated by [serialize]. + static Descendant deserialize(Map json) { + final Map of = {}; + final Map matching = {}; + final Map other = {}; + 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 serialize() { + return super.serialize() + ..addAll(of.serialize().map((String key, String value) => MapEntry('of_$key', value))) + ..addAll(matching.serialize().map((String key, String value) => MapEntry('matching_$key', value))) + ..addAll({ + 'matchRoot': matchRoot ? 'true' : 'false', + }); + } + + /// Deserializes the finder from JSON generated by [serialize]. + static Ancestor deserialize(Map json) { + final Map of = {}; + final Map matching = {}; + final Map other = {}; + 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. /// /// This command requires assertions to be enabled on the device. diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index 585d8ae0c6..9fd3771002 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -1017,6 +1017,28 @@ class CommonFinders { /// Finds the back button on a Material or Cupertino page's scaffold. 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. diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index c7afd09ec8..894d6f9da8 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -142,6 +142,8 @@ class FlutterDriverExtension { 'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder), 'ByType': (SerializableFinder finder) => _createByTypeFinder(finder), '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'); } + 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) { final FinderConstructor constructor = _finders[finder.finderType]; diff --git a/packages/flutter_driver/test/src/extension_test.dart b/packages/flutter_driver/test/src/extension_test.dart index 6e139f16ed..49d1c6c758 100644 --- a/packages/flutter_driver/test/src/extension_test.dart +++ b/packages/flutter_driver/test/src/extension_test.dart @@ -2,12 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.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/geometry.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_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.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 getDescendantText({ String of, bool matchRoot = false}) async { + final Map arguments = GetText(Descendant( + of: ByValueKey(of), + matching: ByValueKey('text2'), + matchRoot: matchRoot, + ), timeout: const Duration(seconds: 1)).serialize(); + final Map 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('column'), + children: const [ + Text('Hello1', key: ValueKey('text1')), + Text('Hello2', key: ValueKey('text2')), + Text('Hello3', key: ValueKey('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 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 getAncestorTopLeft({ String of, String matching, bool matchRoot = false}) async { + final Map arguments = GetOffset(Ancestor( + of: ByValueKey(of), + matching: ByValueKey(matching), + matchRoot: matchRoot, + ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize(); + final Map 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('parent'), + height: 100, + width: 100, + child: Center( + child: Row( + children: [ + Container( + key: const ValueKey('leftchild'), + width: 25, + height: 25, + ), + Container( + key: const ValueKey('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 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); + }); } diff --git a/packages/flutter_driver/test/src/find_test.dart b/packages/flutter_driver/test/src/find_test.dart new file mode 100644 index 0000000000..c48b0400c3 --- /dev/null +++ b/packages/flutter_driver/test/src/find_test.dart @@ -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(), { + '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 serialized = { + '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()); + expect(a.matching, isA()); + 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(), { + '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 serialized = { + '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()); + expect(a.matching, isA()); + expect(a.matchRoot, isTrue); + }); +}