// Copyright 2016 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:meta/meta.dart'; import 'error.dart'; import 'message.dart'; const List<Type> _supportedKeyValueTypes = <Type>[String, int]; DriverError _createInvalidKeyValueTypeError(String invalidType) { return DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}'); } /// A Flutter Driver command aimed at an object to be located by [finder]. /// /// Implementations must provide a concrete [kind]. If additional data is /// required beyond the [finder] the implementation may override [serialize] /// and add more keys to the returned map. abstract class CommandWithTarget extends Command { /// Constructs this command given a [finder]. CommandWithTarget(this.finder, {Duration timeout}) : super(timeout: timeout) { if (finder == null) throw DriverError('$runtimeType target cannot be null'); } /// Deserializes this command from the value generated by [serialize]. CommandWithTarget.deserialize(Map<String, String> json) : finder = SerializableFinder.deserialize(json), super.deserialize(json); /// Locates the object or objects targeted by this command. final SerializableFinder finder; /// This method is meant to be overridden if data in addition to [finder] /// is serialized to JSON. /// /// Example: /// /// Map<String, String> toJson() => super.toJson()..addAll({ /// 'foo': this.foo, /// }); @override Map<String, String> serialize() => super.serialize()..addAll(finder.serialize()); } /// A Flutter Driver command that waits until [finder] can locate the target. class WaitFor extends CommandWithTarget { /// Creates a command that waits for the widget identified by [finder] to /// appear within the [timeout] amount of time. /// /// If [timeout] is not specified the command times out after 5 seconds. WaitFor(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); /// Deserializes this command from the value generated by [serialize]. WaitFor.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'waitFor'; } /// The result of a [WaitFor] command. class WaitForResult extends Result { /// Creates a [WaitForResult]. const WaitForResult(); /// Deserializes the result from JSON. static WaitForResult fromJson(Map<String, dynamic> json) { return const WaitForResult(); } @override Map<String, dynamic> toJson() => <String, dynamic>{}; } /// A Flutter Driver command that waits until [finder] can no longer locate the target. class WaitForAbsent extends CommandWithTarget { /// Creates a command that waits for the widget identified by [finder] to /// disappear within the [timeout] amount of time. /// /// If [timeout] is not specified the command times out after 5 seconds. WaitForAbsent(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); /// Deserializes this command from the value generated by [serialize]. WaitForAbsent.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'waitForAbsent'; } /// The result of a [WaitForAbsent] command. class WaitForAbsentResult extends Result { /// Creates a [WaitForAbsentResult]. const WaitForAbsentResult(); /// Deserializes the result from JSON. static WaitForAbsentResult fromJson(Map<String, dynamic> json) { return const WaitForAbsentResult(); } @override Map<String, dynamic> toJson() => <String, dynamic>{}; } /// A Flutter Driver command that waits until there are no more transient callbacks in the queue. class WaitUntilNoTransientCallbacks extends Command { /// Creates a command that waits for there to be no transient callbacks. const WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout); /// Deserializes this command from the value generated by [serialize]. WaitUntilNoTransientCallbacks.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'waitUntilNoTransientCallbacks'; } /// A Flutter Driver command that waits until the frame is synced. class WaitUntilNoPendingFrame extends Command { /// Creates a command that waits until there's no pending frame scheduled. const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); /// Deserializes this command from the value generated by [serialize]. WaitUntilNoPendingFrame.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'waitUntilNoPendingFrame'; } /// A Flutter Driver command that waits until the Flutter engine rasterizes the /// first frame. /// /// {@template flutter.frame_rasterized_vs_presented} /// Usually, the time that a frame is rasterized is very close to the time that /// it gets presented on the display. Specifically, rasterization is the last /// expensive phase of a frame that's still in Flutter's control. /// {@endtemplate} class WaitUntilFirstFrameRasterized extends Command { /// Creates this command. const WaitUntilFirstFrameRasterized({ Duration timeout }) : super(timeout: timeout); /// Deserializes this command from the value generated by [serialize]. WaitUntilFirstFrameRasterized.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'waitUntilFirstFrameRasterized'; } /// Base class for Flutter Driver finders, objects that describe how the driver /// should search for elements. abstract class SerializableFinder { /// A const constructor to allow subclasses to be const. const SerializableFinder(); /// Identifies the type of finder to be used by the driver extension. String get finderType; /// Serializes common fields to JSON. /// /// Methods that override [serialize] are expected to call `super.serialize` /// and add more fields to the returned [Map]. @mustCallSuper Map<String, String> serialize() => <String, String>{ 'finderType': finderType, }; /// Deserializes a finder from JSON generated by [serialize]. static SerializableFinder deserialize(Map<String, String> json) { final String finderType = json['finderType']; switch (finderType) { 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 const PageBack(); case 'Descendant': return Descendant.deserialize(json); case 'Ancestor': return Ancestor.deserialize(json); } throw DriverError('Unsupported search specification type $finderType'); } } /// A Flutter Driver finder that finds widgets by tooltip text. class ByTooltipMessage extends SerializableFinder { /// Creates a tooltip finder given the tooltip's message [text]. const ByTooltipMessage(this.text); /// Tooltip message text. final String text; @override String get finderType => 'ByTooltipMessage'; @override Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 'text': text, }); /// Deserializes the finder from JSON generated by [serialize]. static ByTooltipMessage deserialize(Map<String, String> json) { return ByTooltipMessage(json['text']); } } /// 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]. const 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 String get 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 { /// Creates a text finder given the text. const ByText(this.text); /// The text that appears inside the [Text] or [EditableText] widget. final String text; @override String get finderType => 'ByText'; @override Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 'text': text, }); /// Deserializes the finder from JSON generated by [serialize]. static ByText deserialize(Map<String, String> json) { return ByText(json['text']); } } /// A Flutter Driver finder that finds widgets by `ValueKey`. class ByValueKey extends SerializableFinder { /// Creates a finder given the key value. ByValueKey(this.keyValue) : keyValueString = '$keyValue', keyValueType = '${keyValue.runtimeType}' { if (!_supportedKeyValueTypes.contains(keyValue.runtimeType)) throw _createInvalidKeyValueTypeError('$keyValue.runtimeType'); } /// The true value of the key. final dynamic keyValue; /// Stringified value of the key (we can only send strings to the VM service) final String keyValueString; /// The type name of the key. /// /// May be one of "String", "int". The list of supported types may change. final String keyValueType; @override String get finderType => 'ByValueKey'; @override Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 'keyValueString': keyValueString, 'keyValueType': keyValueType, }); /// Deserializes the finder from JSON generated by [serialize]. static ByValueKey deserialize(Map<String, String> json) { final String keyValueString = json['keyValueString']; final String keyValueType = json['keyValueType']; switch (keyValueType) { case 'int': return ByValueKey(int.parse(keyValueString)); case 'String': return ByValueKey(keyValueString); default: throw _createInvalidKeyValueTypeError(keyValueType); } } } /// A Flutter Driver finder that finds widgets by their [runtimeType]. class ByType extends SerializableFinder { /// Creates a finder that given the runtime type in string form. const ByType(this.type); /// The widget's [runtimeType], in string form. final String type; @override String get finderType => 'ByType'; @override Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 'type': type, }); /// Deserializes the finder from JSON generated by [serialize]. static ByType deserialize(Map<String, String> json) { return ByType(json['type']); } } /// A Flutter Driver finder that finds the back button on the page's Material /// or Cupertino scaffold. /// /// See also: /// /// * [WidgetTester.pageBack], for a similar functionality in widget tests. class PageBack extends SerializableFinder { /// Creates a [PageBack]. const PageBack(); @override 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. /// /// This command requires assertions to be enabled on the device. /// /// If the object returned by the finder does not have its own semantics node, /// then the semantics node of the first ancestor is returned instead. /// /// Throws an error if a finder returns multiple objects or if there are no /// semantics nodes. /// /// Semantics must be enabled to use this method, either using a platform /// specific shell command or [FlutterDriver.setSemantics]. class GetSemanticsId extends CommandWithTarget { /// Creates a command which finds a Widget and then looks up the semantic id. GetSemanticsId(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); /// Creates a command from a json map. GetSemanticsId.deserialize(Map<String, String> json) : super.deserialize(json); @override String get kind => 'get_semantics_id'; } /// The result of a [GetSemanticsId] command. class GetSemanticsIdResult extends Result { /// Creates a new [GetSemanticsId] result. const GetSemanticsIdResult(this.id); /// The semantics id of the node; final int id; /// Deserializes this result from JSON. static GetSemanticsIdResult fromJson(Map<String, dynamic> json) { return GetSemanticsIdResult(json['id']); } @override Map<String, dynamic> toJson() => <String, dynamic>{'id': id}; }