diff --git a/packages/flutter_driver/lib/src/common/create_finder_factory.dart b/packages/flutter_driver/lib/src/common/create_finder_factory.dart new file mode 100644 index 0000000000..36c3d27e72 --- /dev/null +++ b/packages/flutter_driver/lib/src/common/create_finder_factory.dart @@ -0,0 +1,117 @@ +// Copyright 2014 The Flutter 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_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +import 'error.dart'; +import 'find.dart'; + +/// A factory which creates [Finder]s from [SerializableFinder]s. +mixin CreateFinderFactory { + /// Creates the flutter widget finder from [SerializableFinder]. + Finder createFinder(SerializableFinder finder) { + final String finderType = finder.finderType; + switch (finderType) { + case 'ByText': + return _createByTextFinder(finder as ByText); + case 'ByTooltipMessage': + return _createByTooltipMessageFinder(finder as ByTooltipMessage); + case 'BySemanticsLabel': + return _createBySemanticsLabelFinder(finder as BySemanticsLabel); + case 'ByValueKey': + return _createByValueKeyFinder(finder as ByValueKey); + case 'ByType': + return _createByTypeFinder(finder as ByType); + case 'PageBack': + return _createPageBackFinder(); + case 'Ancestor': + return _createAncestorFinder(finder as Ancestor); + case 'Descendant': + return _createDescendantFinder(finder as Descendant); + default: + throw DriverError('Unsupported search specification type $finderType'); + } + } + + Finder _createByTextFinder(ByText arguments) { + return find.text(arguments.text); + } + + Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) { + return find.byElementPredicate((Element element) { + final Widget widget = element.widget; + if (widget is Tooltip) { + return widget.message == arguments.text; + } + return false; + }, 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': + return find.byKey(ValueKey(arguments.keyValue as int)); + case 'String': + return find.byKey(ValueKey(arguments.keyValue as String)); + default: + throw 'Unsupported ByValueKey type: ${arguments.keyValueType}'; + } + } + + Finder _createByTypeFinder(ByType arguments) { + return find.byElementPredicate((Element element) { + return element.widget.runtimeType.toString() == arguments.type; + }, description: 'widget with runtimeType "${arguments.type}"'); + } + + Finder _createPageBackFinder() { + return find.byElementPredicate((Element element) { + final Widget widget = element.widget; + if (widget is Tooltip) { + return widget.message == 'Back'; + } + if (widget is CupertinoNavigationBarBackButton) { + return true; + } + return false; + }, description: 'Material or Cupertino back button'); + } + + Finder _createAncestorFinder(Ancestor arguments) { + 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) { + final Finder finder = find.descendant( + of: createFinder(arguments.of), + matching: createFinder(arguments.matching), + matchRoot: arguments.matchRoot, + ); + return arguments.firstMatchOnly ? finder.first : finder; + } +} diff --git a/packages/flutter_driver/lib/src/extension/extension.dart b/packages/flutter_driver/lib/src/extension/extension.dart index 9614d52872..44bc9622bd 100644 --- a/packages/flutter_driver/lib/src/extension/extension.dart +++ b/packages/flutter_driver/lib/src/extension/extension.dart @@ -16,6 +16,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../common/create_finder_factory.dart'; import '../common/diagnostics_tree.dart'; import '../common/error.dart'; import '../common/find.dart'; @@ -123,7 +124,7 @@ class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, /// return Some(json['title']); /// } /// -/// Finder createFinder(SerializableFinder finder) { +/// Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory) { /// Some someFinder = finder as Some; /// /// return find.byElementPredicate((Element element) { @@ -156,11 +157,13 @@ abstract class FinderExtension { String get finderType; /// Deserializes the finder from JSON generated by [SerializableFinder.serialize]. + /// [finderFactory] could be used to deserialize nested finders. SerializableFinder deserialize(Map params, DeserializeFinderFactory finderFactory); /// Signature for functions that run the given finder and return the [Element] /// found, if any, or null otherwise. - Finder createFinder(SerializableFinder finder); + /// [finderFactory] could be used to create nested finders. + Finder createFinder(SerializableFinder finder, CreateFinderFactory finderFactory); } /// The class that manages communication between a Flutter Driver test and the @@ -169,7 +172,7 @@ abstract class FinderExtension { /// This is not normally used directly. It is instantiated automatically when /// calling [enableFlutterDriverExtension]. @visibleForTesting -class FlutterDriverExtension with DeserializeFinderFactory { +class FlutterDriverExtension with DeserializeFinderFactory, CreateFinderFactory { /// Creates an object to manage a Flutter Driver connection. FlutterDriverExtension( this._requestDataHandler, @@ -354,112 +357,29 @@ class FlutterDriverExtension with DeserializeFinderFactory { return finder; } - Finder _createByTextFinder(ByText arguments) { - return find.text(arguments.text); - } - - Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) { - return find.byElementPredicate((Element element) { - final Widget widget = element.widget; - if (widget is Tooltip) - return widget.message == arguments.text; - return false; - }, 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': - return find.byKey(ValueKey(arguments.keyValue as int)); - case 'String': - return find.byKey(ValueKey(arguments.keyValue as String)); - default: - throw 'Unsupported ByValueKey type: ${arguments.keyValueType}'; + @override + SerializableFinder deserializeFinder(Map json) { + final String? finderType = json['finderType']; + if (_finderExtensions.containsKey(finderType)) { + return _finderExtensions[finderType]!.deserialize(json, this); } + + return super.deserializeFinder(json); } - Finder _createByTypeFinder(ByType arguments) { - return find.byElementPredicate((Element element) { - return element.widget.runtimeType.toString() == arguments.type; - }, description: 'widget with runtimeType "${arguments.type}"'); - } - - Finder _createPageBackFinder() { - return find.byElementPredicate((Element element) { - final Widget widget = element.widget; - if (widget is Tooltip) - return widget.message == 'Back'; - if (widget is CupertinoNavigationBarBackButton) - return true; - return false; - }, description: 'Material or Cupertino back button'); - } - - Finder _createAncestorFinder(Ancestor arguments) { - 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) { - 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) { - switch (finder.finderType) { - case 'ByText': - return _createByTextFinder(finder as ByText); - case 'ByTooltipMessage': - return _createByTooltipMessageFinder(finder as ByTooltipMessage); - case 'BySemanticsLabel': - return _createBySemanticsLabelFinder(finder as BySemanticsLabel); - case 'ByValueKey': - return _createByValueKeyFinder(finder as ByValueKey); - case 'ByType': - return _createByTypeFinder(finder as ByType); - case 'PageBack': - return _createPageBackFinder(); - case 'Ancestor': - return _createAncestorFinder(finder as Ancestor); - case 'Descendant': - return _createDescendantFinder(finder as Descendant); - default: - if (_finderExtensions.containsKey(finder.finderType)) { - return _finderExtensions[finder.finderType]!.createFinder(finder); - } else { - throw 'Unsupported finder type: ${finder.finderType}'; - } + @override + Finder createFinder(SerializableFinder finder) { + if (_finderExtensions.containsKey(finder.finderType)) { + return _finderExtensions[finder.finderType]!.createFinder(finder, this); } + + return super.createFinder(finder); } Future _tap(Command command) async { final Tap tapCommand = command as Tap; final Finder computedFinder = await _waitForElement( - _createFinder(tapCommand.finder).hitTestable() + createFinder(tapCommand.finder).hitTestable() ); await _prober.tap(computedFinder); return const TapResult(); @@ -467,13 +387,13 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _waitFor(Command command) async { final WaitFor waitForCommand = command as WaitFor; - await _waitForElement(_createFinder(waitForCommand.finder)); + await _waitForElement(createFinder(waitForCommand.finder)); return const WaitForResult(); } Future _waitForAbsent(Command command) async { final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent; - await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder)); + await _waitForAbsentElement(createFinder(waitForAbsentCommand.finder)); return const WaitForAbsentResult(); } @@ -528,7 +448,7 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _getSemanticsId(Command command) async { final GetSemanticsId semanticsCommand = command as GetSemanticsId; - final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder)); + final Finder target = await _waitForElement(createFinder(semanticsCommand.finder)); final Iterable elements = target.evaluate(); if (elements.length > 1) { throw StateError('Found more than one element with the same ID: $elements'); @@ -547,7 +467,7 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _getOffset(Command command) async { final GetOffset getOffsetCommand = command as GetOffset; - final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder)); + final Finder finder = await _waitForElement(createFinder(getOffsetCommand.finder)); final Element element = finder.evaluate().single; final RenderBox box = (element.renderObject as RenderBox?)!; Offset localPoint; @@ -574,7 +494,7 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _getDiagnosticsTree(Command command) async { final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree; - final Finder finder = await _waitForElement(_createFinder(diagnosticsCommand.finder)); + final Finder finder = await _waitForElement(createFinder(diagnosticsCommand.finder)); final Element element = finder.evaluate().single; DiagnosticsNode diagnosticsNode; switch (diagnosticsCommand.diagnosticsType) { @@ -593,7 +513,7 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _scroll(Command command) async { final Scroll scrollCommand = command as Scroll; - final Finder target = await _waitForElement(_createFinder(scrollCommand.finder)); + final Finder target = await _waitForElement(createFinder(scrollCommand.finder)); final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond; final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble(); final Duration pause = scrollCommand.duration ~/ totalMoves; @@ -614,18 +534,14 @@ class FlutterDriverExtension with DeserializeFinderFactory { Future _scrollIntoView(Command command) async { final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView; - final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder)); - await Scrollable.ensureVisible( - target.evaluate().single, - duration: const Duration(milliseconds: 100), - alignment: scrollIntoViewCommand.alignment, - ); + final Finder target = await _waitForElement(createFinder(scrollIntoViewCommand.finder)); + await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment); return const ScrollResult(); } Future _getText(Command command) async { final GetText getTextCommand = command as GetText; - final Finder target = await _waitForElement(_createFinder(getTextCommand.finder)); + final Finder target = await _waitForElement(createFinder(getTextCommand.finder)); final Widget widget = target.evaluate().single.widget; String? text; diff --git a/packages/flutter_driver/test/src/real_tests/extension_test.dart b/packages/flutter_driver/test/src/real_tests/extension_test.dart index 968cb93be8..7c036bbe2b 100644 --- a/packages/flutter_driver/test/src/real_tests/extension_test.dart +++ b/packages/flutter_driver/test/src/real_tests/extension_test.dart @@ -20,6 +20,9 @@ import 'package:flutter_driver/src/common/wait.dart'; import 'package:flutter_driver/src/extension/extension.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'stubs/stub_finder.dart'; +import 'stubs/stub_finder_extension.dart'; + Future silenceDriverLogger(AsyncCallback callback) async { final DriverLogCallback oldLogger = driverLog; driverLog = (String source, String message) { }; @@ -32,18 +35,18 @@ Future silenceDriverLogger(AsyncCallback callback) async { void main() { group('waitUntilNoTransientCallbacks', () { - FlutterDriverExtension extension; + FlutterDriverExtension driverExtension; Map result; int messageId = 0; final List log = []; setUp(() { result = null; - extension = FlutterDriverExtension((String message) async { log.add(message); return (messageId += 1).toString(); }, false); + driverExtension = FlutterDriverExtension((String message) async { log.add(message); return (messageId += 1).toString(); }, false); }); testWidgets('returns immediately when transient callback queue is empty', (WidgetTester tester) async { - extension.call(const WaitUntilNoTransientCallbacks().serialize()) + driverExtension.call(const WaitUntilNoTransientCallbacks().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -63,7 +66,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoTransientCallbacks().serialize()) + driverExtension.call(const WaitUntilNoTransientCallbacks().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -85,7 +88,7 @@ void main() { testWidgets('handler', (WidgetTester tester) async { expect(log, isEmpty); - final Map response = await extension.call(const RequestData('hello').serialize()); + final Map response = await driverExtension.call(const RequestData('hello').serialize()); final RequestDataResult result = RequestDataResult.fromJson(response['response'] as Map); expect(log, ['hello']); expect(result.message, '1'); @@ -93,18 +96,18 @@ void main() { }); group('waitForCondition', () { - FlutterDriverExtension extension; + FlutterDriverExtension driverExtension; Map result; int messageId = 0; final List log = []; setUp(() { result = null; - extension = FlutterDriverExtension((String message) async { log.add(message); return (messageId += 1).toString(); }, false); + driverExtension = FlutterDriverExtension((String message) async { log.add(message); return (messageId += 1).toString(); }, false); }); testWidgets('waiting for NoTransientCallbacks returns immediately when transient callback queue is empty', (WidgetTester tester) async { - extension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) + driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -124,7 +127,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) + driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -146,7 +149,7 @@ void main() { testWidgets('waiting for NoPendingFrame returns immediately when frame is synced', ( WidgetTester tester) async { - extension.call(const WaitForCondition(NoPendingFrame()).serialize()) + driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -164,7 +167,7 @@ void main() { testWidgets('waiting for NoPendingFrame returns until no pending scheduled frame', (WidgetTester tester) async { SchedulerBinding.instance.scheduleFrame(); - extension.call(const WaitForCondition(NoPendingFrame()).serialize()) + driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -188,7 +191,7 @@ void main() { 'waiting for combined conditions returns immediately', (WidgetTester tester) async { const SerializableWaitCondition combinedCondition = CombinedCondition([NoTransientCallbacks(), NoPendingFrame()]); - extension.call(const WaitForCondition(combinedCondition).serialize()) + driverExtension.call(const WaitForCondition(combinedCondition).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -212,7 +215,7 @@ void main() { const SerializableWaitCondition combinedCondition = CombinedCondition([NoTransientCallbacks(), NoPendingFrame()]); - extension.call(const WaitForCondition(combinedCondition).serialize()) + driverExtension.call(const WaitForCondition(combinedCondition).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -241,7 +244,7 @@ void main() { const SerializableWaitCondition combinedCondition = CombinedCondition([NoPendingFrame(), NoTransientCallbacks()]); - extension.call(const WaitForCondition(combinedCondition).serialize()) + driverExtension.call(const WaitForCondition(combinedCondition).serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -263,7 +266,7 @@ void main() { testWidgets( "waiting for NoPendingPlatformMessages returns immediately when there're no platform messages", (WidgetTester tester) async { - extension + driverExtension .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) .then(expectAsync1((Map r) { result = r; @@ -291,7 +294,7 @@ void main() { }); channel.invokeMethod('sayHello', 'hello'); - extension + driverExtension .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) .then(expectAsync1((Map r) { result = r; @@ -336,7 +339,7 @@ void main() { channel1.invokeMethod('sayHello', 'hello'); channel2.invokeMethod('sayHello', 'hello'); - extension + driverExtension .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) .then(expectAsync1((Map r) { result = r; @@ -385,7 +388,7 @@ void main() { channel1.invokeMethod('sayHello', 'hello'); // Calls the waiting API before the second channel message is sent. - extension + driverExtension .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) .then(expectAsync1((Map r) { result = r; @@ -435,7 +438,7 @@ void main() { channel1.invokeMethod('sayHello', 'hello'); - extension + driverExtension .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) .then(expectAsync1((Map r) { result = r; @@ -464,9 +467,9 @@ void main() { }); group('getSemanticsId', () { - FlutterDriverExtension extension; + FlutterDriverExtension driverExtension; setUp(() { - extension = FlutterDriverExtension((String arg) async => '', true); + driverExtension = FlutterDriverExtension((String arg) async => '', true); }); testWidgets('works when semantics are enabled', (WidgetTester tester) async { @@ -475,7 +478,7 @@ void main() { const Text('hello', textDirection: TextDirection.ltr)); final Map arguments = GetSemanticsId(const ByText('hello')).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(response['response'] as Map); expect(result.id, 1); @@ -487,7 +490,7 @@ void main() { const Text('hello', textDirection: TextDirection.ltr)); final Map arguments = GetSemanticsId(const ByText('hello')).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); expect(response['isError'], true); expect(response['response'], contains('Bad state: No semantics data found')); @@ -506,7 +509,7 @@ void main() { ); final Map arguments = GetSemanticsId(const ByText('hello')).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); expect(response['isError'], true); expect(response['response'], contains('Bad state: Found more than one element with the same ID')); @@ -515,11 +518,11 @@ void main() { }); testWidgets('getOffset', (WidgetTester tester) async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getOffset(OffsetType offset) async { final Map arguments = GetOffset(ByValueKey(1), offset).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map); return Offset(result.dx, result.dy); } @@ -547,11 +550,11 @@ void main() { testWidgets('getText', (WidgetTester tester) async { await silenceDriverLogger(() async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getTextInternal(SerializableFinder search) async { final Map arguments = GetText(search, timeout: const Duration(seconds: 1)).serialize(); - final Map result = await extension.call(arguments); + final Map result = await driverExtension.call(arguments); if (result['isError'] as bool) { return null; } @@ -609,7 +612,7 @@ void main() { // Check if error thrown for other types final Map arguments = GetText(ByValueKey('column'), timeout: const Duration(seconds: 1)).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); expect(response['isError'], true); expect(response['response'], contains('is currently not supported by getText')); }); @@ -617,7 +620,7 @@ void main() { testWidgets('descendant finder', (WidgetTester tester) async { await silenceDriverLogger(() async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getDescendantText({ String of, bool matchRoot = false}) async { final Map arguments = GetText(Descendant( @@ -625,7 +628,7 @@ void main() { matching: ByValueKey('text2'), matchRoot: matchRoot, ), timeout: const Duration(seconds: 1)).serialize(); - final Map result = await extension.call(arguments); + final Map result = await driverExtension.call(arguments); if (result['isError'] as bool) { return null; } @@ -662,7 +665,7 @@ void main() { testWidgets('descendant finder firstMatchOnly', (WidgetTester tester) async { await silenceDriverLogger(() async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getDescendantText() async { final Map arguments = GetText(Descendant( @@ -670,7 +673,7 @@ void main() { matching: const ByType('Text'), firstMatchOnly: true, ), timeout: const Duration(seconds: 1)).serialize(); - final Map result = await extension.call(arguments); + final Map result = await driverExtension.call(arguments); if (result['isError'] as bool) { return null; } @@ -696,7 +699,7 @@ void main() { testWidgets('ancestor finder', (WidgetTester tester) async { await silenceDriverLogger(() async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getAncestorTopLeft({ String of, String matching, bool matchRoot = false}) async { final Map arguments = GetOffset(Ancestor( @@ -704,7 +707,7 @@ void main() { matching: ByValueKey(matching), matchRoot: matchRoot, ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); if (response['isError'] as bool) { return null; } @@ -766,7 +769,7 @@ void main() { testWidgets('ancestor finder firstMatchOnly', (WidgetTester tester) async { await silenceDriverLogger(() async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future getAncestorTopLeft() async { final Map arguments = GetOffset(Ancestor( @@ -774,7 +777,7 @@ void main() { matching: const ByType('Container'), firstMatchOnly: true, ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); if (response['isError'] as bool) { return null; } @@ -814,11 +817,11 @@ void main() { }); testWidgets('GetDiagnosticsTree', (WidgetTester tester) async { - final FlutterDriverExtension extension = FlutterDriverExtension((String arg) async => '', true); + final FlutterDriverExtension driverExtension = FlutterDriverExtension((String arg) async => '', true); Future> getDiagnosticsTree(DiagnosticsType type, SerializableFinder finder, { int depth = 0, bool properties = true }) async { final Map arguments = GetDiagnosticsTree(finder, type, subtreeDepth: depth, includeProperties: properties).serialize(); - final Map response = await extension.call(arguments); + final Map response = await driverExtension.call(arguments); final DiagnosticsTreeResult result = DiagnosticsTreeResult(response['response'] as Map); return result.json; } @@ -879,18 +882,120 @@ void main() { expect(children.single['children'], isEmpty); }); + group('extension finders', () { + final Widget debugTree = Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Column( + key: const ValueKey('Column'), + children: [ + const Text('Foo', key: ValueKey('Text1')), + const Text('Bar', key: ValueKey('Text2')), + FlatButton( + child: const Text('Whatever'), + key: const ValueKey('Button'), + onPressed: () {}, + ), + ], + ), + ), + ); + + testWidgets('unknown extension finder', (WidgetTester tester) async { + final FlutterDriverExtension driverExtension = FlutterDriverExtension( + (String arg) async => '', + true, + finders: [], + ); + + Future> getText(SerializableFinder finder) async { + final Map arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); + return await driverExtension.call(arguments); + } + + await tester.pumpWidget(debugTree); + + final Map result = await getText(StubFinder('Text1')); + expect(result['isError'], true); + expect(result['response'] is String, true); + expect(result['response'] as String, contains('Unsupported search specification type Stub')); + }); + + testWidgets('simple extension finder', (WidgetTester tester) async { + final FlutterDriverExtension driverExtension = FlutterDriverExtension( + (String arg) async => '', + true, + finders: [ + StubFinderExtension(), + ], + ); + + Future getText(SerializableFinder finder) async { + final Map arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); + final Map response = await driverExtension.call(arguments); + return GetTextResult.fromJson(response['response'] as Map); + } + + await tester.pumpWidget(debugTree); + + final GetTextResult result = await getText(StubFinder('Text1')); + expect(result.text, 'Foo'); + }); + + testWidgets('complex extension finder', (WidgetTester tester) async { + final FlutterDriverExtension driverExtension = FlutterDriverExtension( + (String arg) async => '', + true, + finders: [ + StubFinderExtension(), + ], + ); + + Future getText(SerializableFinder finder) async { + final Map arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); + final Map response = await driverExtension.call(arguments); + return GetTextResult.fromJson(response['response'] as Map); + } + + await tester.pumpWidget(debugTree); + + final GetTextResult result = await getText(Descendant(of: StubFinder('Column'), matching: StubFinder('Text1'))); + expect(result.text, 'Foo'); + }); + + testWidgets('extension finder with command', (WidgetTester tester) async { + final FlutterDriverExtension driverExtension = FlutterDriverExtension( + (String arg) async => '', + true, + finders: [ + StubFinderExtension(), + ], + ); + + Future> tap(SerializableFinder finder) async { + final Map arguments = Tap(finder, timeout: const Duration(seconds: 1)).serialize(); + return await driverExtension.call(arguments); + } + + await tester.pumpWidget(debugTree); + + final Map result = await tap(StubFinder('Button')); + expect(result['isError'], false); + }); + }); + group('waitUntilFrameSync', () { - FlutterDriverExtension extension; + FlutterDriverExtension driverExtension; Map result; setUp(() { - extension = FlutterDriverExtension((String arg) async => '', true); + driverExtension = FlutterDriverExtension((String arg) async => '', true); result = null; }); testWidgets('returns immediately when frame is synced', ( WidgetTester tester) async { - extension.call(const WaitUntilNoPendingFrame().serialize()) + driverExtension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -911,7 +1016,7 @@ void main() { // Intentionally blank. We only care about existence of a callback. }); - extension.call(const WaitUntilNoPendingFrame().serialize()) + driverExtension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; })); @@ -935,7 +1040,7 @@ void main() { 'waits until no pending scheduled frame', (WidgetTester tester) async { SchedulerBinding.instance.scheduleFrame(); - extension.call(const WaitUntilNoPendingFrame().serialize()) + driverExtension.call(const WaitUntilNoPendingFrame().serialize()) .then(expectAsync1((Map r) { result = r; })); diff --git a/packages/flutter_driver/test/src/real_tests/stubs/stub_finder.dart b/packages/flutter_driver/test/src/real_tests/stubs/stub_finder.dart new file mode 100644 index 0000000000..c8ea977eef --- /dev/null +++ b/packages/flutter_driver/test/src/real_tests/stubs/stub_finder.dart @@ -0,0 +1,19 @@ +// Copyright 2014 The Flutter 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/flutter_driver.dart'; + +class StubFinder extends SerializableFinder { + StubFinder(this.keyString); + + final String keyString; + + @override + String get finderType => 'Stub'; + + @override + Map serialize() { + return super.serialize()..addAll({'keyString': keyString}); + } +} diff --git a/packages/flutter_driver/test/src/real_tests/stubs/stub_finder_extension.dart b/packages/flutter_driver/test/src/real_tests/stubs/stub_finder_extension.dart new file mode 100644 index 0000000000..6b5694ed5c --- /dev/null +++ b/packages/flutter_driver/test/src/real_tests/stubs/stub_finder_extension.dart @@ -0,0 +1,38 @@ +// Copyright 2014 The Flutter 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/src/widgets/framework.dart'; +import 'package:flutter_driver/driver_extension.dart'; +import 'package:flutter_driver/src/common/create_finder_factory.dart'; +import 'package:flutter_test/src/finders.dart'; +import 'package:flutter_driver/src/common/find.dart'; + +import 'stub_finder.dart'; + +class StubFinderExtension extends FinderExtension { + @override + Finder createFinder( + SerializableFinder finder, + CreateFinderFactory finderFactory, + ) { + return find.byWidgetPredicate((Widget widget) { + final Key? key = widget.key; + if (key is! ValueKey) { + return false; + } + return key.value == (finder as StubFinder).keyString; + }); + } + + @override + SerializableFinder deserialize( + Map params, + DeserializeFinderFactory finderFactory, + ) { + return StubFinder(params['keyString']!); + } + + @override + String get finderType => 'Stub'; +}