1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:meta/meta.dart'; 6 7import 'error.dart'; 8import 'message.dart'; 9 10const List<Type> _supportedKeyValueTypes = <Type>[String, int]; 11 12DriverError _createInvalidKeyValueTypeError(String invalidType) { 13 return DriverError('Unsupported key value type $invalidType. Flutter Driver only supports ${_supportedKeyValueTypes.join(", ")}'); 14} 15 16/// A Flutter Driver command aimed at an object to be located by [finder]. 17/// 18/// Implementations must provide a concrete [kind]. If additional data is 19/// required beyond the [finder] the implementation may override [serialize] 20/// and add more keys to the returned map. 21abstract class CommandWithTarget extends Command { 22 /// Constructs this command given a [finder]. 23 CommandWithTarget(this.finder, {Duration timeout}) : super(timeout: timeout) { 24 if (finder == null) 25 throw DriverError('$runtimeType target cannot be null'); 26 } 27 28 /// Deserializes this command from the value generated by [serialize]. 29 CommandWithTarget.deserialize(Map<String, String> json) 30 : finder = SerializableFinder.deserialize(json), 31 super.deserialize(json); 32 33 /// Locates the object or objects targeted by this command. 34 final SerializableFinder finder; 35 36 /// This method is meant to be overridden if data in addition to [finder] 37 /// is serialized to JSON. 38 /// 39 /// Example: 40 /// 41 /// Map<String, String> toJson() => super.toJson()..addAll({ 42 /// 'foo': this.foo, 43 /// }); 44 @override 45 Map<String, String> serialize() => 46 super.serialize()..addAll(finder.serialize()); 47} 48 49/// A Flutter Driver command that waits until [finder] can locate the target. 50class WaitFor extends CommandWithTarget { 51 /// Creates a command that waits for the widget identified by [finder] to 52 /// appear within the [timeout] amount of time. 53 /// 54 /// If [timeout] is not specified the command times out after 5 seconds. 55 WaitFor(SerializableFinder finder, {Duration timeout}) 56 : super(finder, timeout: timeout); 57 58 /// Deserializes this command from the value generated by [serialize]. 59 WaitFor.deserialize(Map<String, String> json) : super.deserialize(json); 60 61 @override 62 String get kind => 'waitFor'; 63} 64 65/// The result of a [WaitFor] command. 66class WaitForResult extends Result { 67 /// Creates a [WaitForResult]. 68 const WaitForResult(); 69 70 /// Deserializes the result from JSON. 71 static WaitForResult fromJson(Map<String, dynamic> json) { 72 return const WaitForResult(); 73 } 74 75 @override 76 Map<String, dynamic> toJson() => <String, dynamic>{}; 77} 78 79/// A Flutter Driver command that waits until [finder] can no longer locate the target. 80class WaitForAbsent extends CommandWithTarget { 81 /// Creates a command that waits for the widget identified by [finder] to 82 /// disappear within the [timeout] amount of time. 83 /// 84 /// If [timeout] is not specified the command times out after 5 seconds. 85 WaitForAbsent(SerializableFinder finder, {Duration timeout}) 86 : super(finder, timeout: timeout); 87 88 /// Deserializes this command from the value generated by [serialize]. 89 WaitForAbsent.deserialize(Map<String, String> json) : super.deserialize(json); 90 91 @override 92 String get kind => 'waitForAbsent'; 93} 94 95/// The result of a [WaitForAbsent] command. 96class WaitForAbsentResult extends Result { 97 /// Creates a [WaitForAbsentResult]. 98 const WaitForAbsentResult(); 99 100 /// Deserializes the result from JSON. 101 static WaitForAbsentResult fromJson(Map<String, dynamic> json) { 102 return const WaitForAbsentResult(); 103 } 104 105 @override 106 Map<String, dynamic> toJson() => <String, dynamic>{}; 107} 108 109/// A Flutter Driver command that waits until there are no more transient callbacks in the queue. 110class WaitUntilNoTransientCallbacks extends Command { 111 /// Creates a command that waits for there to be no transient callbacks. 112 const WaitUntilNoTransientCallbacks({ Duration timeout }) : super(timeout: timeout); 113 114 /// Deserializes this command from the value generated by [serialize]. 115 WaitUntilNoTransientCallbacks.deserialize(Map<String, String> json) 116 : super.deserialize(json); 117 118 @override 119 String get kind => 'waitUntilNoTransientCallbacks'; 120} 121 122/// A Flutter Driver command that waits until the frame is synced. 123class WaitUntilNoPendingFrame extends Command { 124 /// Creates a command that waits until there's no pending frame scheduled. 125 const WaitUntilNoPendingFrame({ Duration timeout }) : super(timeout: timeout); 126 127 /// Deserializes this command from the value generated by [serialize]. 128 WaitUntilNoPendingFrame.deserialize(Map<String, String> json) 129 : super.deserialize(json); 130 131 @override 132 String get kind => 'waitUntilNoPendingFrame'; 133} 134 135/// A Flutter Driver command that waits until the Flutter engine rasterizes the 136/// first frame. 137/// 138/// {@template flutter.frame_rasterized_vs_presented} 139/// Usually, the time that a frame is rasterized is very close to the time that 140/// it gets presented on the display. Specifically, rasterization is the last 141/// expensive phase of a frame that's still in Flutter's control. 142/// {@endtemplate} 143class WaitUntilFirstFrameRasterized extends Command { 144 /// Creates this command. 145 const WaitUntilFirstFrameRasterized({ Duration timeout }) : super(timeout: timeout); 146 147 /// Deserializes this command from the value generated by [serialize]. 148 WaitUntilFirstFrameRasterized.deserialize(Map<String, String> json) 149 : super.deserialize(json); 150 151 @override 152 String get kind => 'waitUntilFirstFrameRasterized'; 153} 154 155/// Base class for Flutter Driver finders, objects that describe how the driver 156/// should search for elements. 157abstract class SerializableFinder { 158 159 /// A const constructor to allow subclasses to be const. 160 const SerializableFinder(); 161 162 /// Identifies the type of finder to be used by the driver extension. 163 String get finderType; 164 165 /// Serializes common fields to JSON. 166 /// 167 /// Methods that override [serialize] are expected to call `super.serialize` 168 /// and add more fields to the returned [Map]. 169 @mustCallSuper 170 Map<String, String> serialize() => <String, String>{ 171 'finderType': finderType, 172 }; 173 174 /// Deserializes a finder from JSON generated by [serialize]. 175 static SerializableFinder deserialize(Map<String, String> json) { 176 final String finderType = json['finderType']; 177 switch (finderType) { 178 case 'ByType': return ByType.deserialize(json); 179 case 'ByValueKey': return ByValueKey.deserialize(json); 180 case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json); 181 case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json); 182 case 'ByText': return ByText.deserialize(json); 183 case 'PageBack': return const PageBack(); 184 case 'Descendant': return Descendant.deserialize(json); 185 case 'Ancestor': return Ancestor.deserialize(json); 186 } 187 throw DriverError('Unsupported search specification type $finderType'); 188 } 189} 190 191/// A Flutter Driver finder that finds widgets by tooltip text. 192class ByTooltipMessage extends SerializableFinder { 193 /// Creates a tooltip finder given the tooltip's message [text]. 194 const ByTooltipMessage(this.text); 195 196 /// Tooltip message text. 197 final String text; 198 199 @override 200 String get finderType => 'ByTooltipMessage'; 201 202 @override 203 Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 204 'text': text, 205 }); 206 207 /// Deserializes the finder from JSON generated by [serialize]. 208 static ByTooltipMessage deserialize(Map<String, String> json) { 209 return ByTooltipMessage(json['text']); 210 } 211} 212 213/// A Flutter Driver finder that finds widgets by semantic label. 214/// 215/// If the [label] property is a [String], the finder will try to find an exact 216/// match. If it is a [RegExp], it will return true for [RegExp.hasMatch]. 217class BySemanticsLabel extends SerializableFinder { 218 /// Creates a semantic label finder given the [label]. 219 const BySemanticsLabel(this.label); 220 221 /// A [Pattern] matching the [Semantics.properties.label]. 222 /// 223 /// If this is a [String], it will be treated as an exact match. 224 final Pattern label; 225 226 @override 227 String get finderType => 'BySemanticsLabel'; 228 229 @override 230 Map<String, String> serialize() { 231 if (label is RegExp) { 232 final RegExp regExp = label; 233 return super.serialize()..addAll(<String, String>{ 234 'label': regExp.pattern, 235 'isRegExp': 'true', 236 }); 237 } else { 238 return super.serialize()..addAll(<String, String>{ 239 'label': label, 240 }); 241 } 242 } 243 244 /// Deserializes the finder from JSON generated by [serialize]. 245 static BySemanticsLabel deserialize(Map<String, String> json) { 246 final bool isRegExp = json['isRegExp'] == 'true'; 247 return BySemanticsLabel(isRegExp ? RegExp(json['label']) : json['label']); 248 } 249} 250 251/// A Flutter Driver finder that finds widgets by [text] inside a [Text] or 252/// [EditableText] widget. 253class ByText extends SerializableFinder { 254 /// Creates a text finder given the text. 255 const ByText(this.text); 256 257 /// The text that appears inside the [Text] or [EditableText] widget. 258 final String text; 259 260 @override 261 String get finderType => 'ByText'; 262 263 @override 264 Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 265 'text': text, 266 }); 267 268 /// Deserializes the finder from JSON generated by [serialize]. 269 static ByText deserialize(Map<String, String> json) { 270 return ByText(json['text']); 271 } 272} 273 274/// A Flutter Driver finder that finds widgets by `ValueKey`. 275class ByValueKey extends SerializableFinder { 276 /// Creates a finder given the key value. 277 ByValueKey(this.keyValue) 278 : keyValueString = '$keyValue', 279 keyValueType = '${keyValue.runtimeType}' { 280 if (!_supportedKeyValueTypes.contains(keyValue.runtimeType)) 281 throw _createInvalidKeyValueTypeError('$keyValue.runtimeType'); 282 } 283 284 /// The true value of the key. 285 final dynamic keyValue; 286 287 /// Stringified value of the key (we can only send strings to the VM service) 288 final String keyValueString; 289 290 /// The type name of the key. 291 /// 292 /// May be one of "String", "int". The list of supported types may change. 293 final String keyValueType; 294 295 @override 296 String get finderType => 'ByValueKey'; 297 298 @override 299 Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 300 'keyValueString': keyValueString, 301 'keyValueType': keyValueType, 302 }); 303 304 /// Deserializes the finder from JSON generated by [serialize]. 305 static ByValueKey deserialize(Map<String, String> json) { 306 final String keyValueString = json['keyValueString']; 307 final String keyValueType = json['keyValueType']; 308 switch (keyValueType) { 309 case 'int': 310 return ByValueKey(int.parse(keyValueString)); 311 case 'String': 312 return ByValueKey(keyValueString); 313 default: 314 throw _createInvalidKeyValueTypeError(keyValueType); 315 } 316 } 317} 318 319/// A Flutter Driver finder that finds widgets by their [runtimeType]. 320class ByType extends SerializableFinder { 321 /// Creates a finder that given the runtime type in string form. 322 const ByType(this.type); 323 324 /// The widget's [runtimeType], in string form. 325 final String type; 326 327 @override 328 String get finderType => 'ByType'; 329 330 @override 331 Map<String, String> serialize() => super.serialize()..addAll(<String, String>{ 332 'type': type, 333 }); 334 335 /// Deserializes the finder from JSON generated by [serialize]. 336 static ByType deserialize(Map<String, String> json) { 337 return ByType(json['type']); 338 } 339} 340 341/// A Flutter Driver finder that finds the back button on the page's Material 342/// or Cupertino scaffold. 343/// 344/// See also: 345/// 346/// * [WidgetTester.pageBack], for a similar functionality in widget tests. 347class PageBack extends SerializableFinder { 348 /// Creates a [PageBack]. 349 const PageBack(); 350 351 @override 352 String get finderType => 'PageBack'; 353} 354 355/// A Flutter Driver finder that finds a descendant of [of] that matches 356/// [matching]. 357/// 358/// If the `matchRoot` argument is true, then the widget specified by [of] will 359/// be considered for a match. The argument defaults to false. 360class Descendant extends SerializableFinder { 361 /// Creates a descendant finder. 362 const Descendant({ 363 @required this.of, 364 @required this.matching, 365 this.matchRoot = false, 366 }); 367 368 /// The finder specifying the widget of which the descendant is to be found. 369 final SerializableFinder of; 370 371 /// Only a descendant of [of] matching this finder will be found. 372 final SerializableFinder matching; 373 374 /// Whether the widget matching [of] will be considered for a match. 375 final bool matchRoot; 376 377 @override 378 String get finderType => 'Descendant'; 379 380 @override 381 Map<String, String> serialize() { 382 return super.serialize() 383 ..addAll(of.serialize().map((String key, String value) => MapEntry<String, String>('of_$key', value))) 384 ..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value))) 385 ..addAll(<String, String>{ 386 'matchRoot': matchRoot ? 'true' : 'false', 387 }); 388 } 389 390 /// Deserializes the finder from JSON generated by [serialize]. 391 static Descendant deserialize(Map<String, String> json) { 392 final Map<String, String> of = <String, String>{}; 393 final Map<String, String> matching = <String, String>{}; 394 final Map<String, String> other = <String, String>{}; 395 for (String key in json.keys) { 396 if (key.startsWith('of_')) { 397 of[key.substring('of_'.length)] = json[key]; 398 } else if (key.startsWith('matching_')) { 399 matching[key.substring('matching_'.length)] = json[key]; 400 } else { 401 other[key] = json[key]; 402 } 403 } 404 return Descendant( 405 of: SerializableFinder.deserialize(of), 406 matching: SerializableFinder.deserialize(matching), 407 matchRoot: other['matchRoot'] == 'true', 408 ); 409 } 410} 411 412/// A Flutter Driver finder that finds an ancestor of [of] that matches 413/// [matching]. 414/// 415/// If the `matchRoot` argument is true, then the widget specified by [of] will 416/// be considered for a match. The argument defaults to false. 417class Ancestor extends SerializableFinder { 418 /// Creates an ancestor finder. 419 const Ancestor({ 420 @required this.of, 421 @required this.matching, 422 this.matchRoot = false, 423 }); 424 425 /// The finder specifying the widget of which the ancestor is to be found. 426 final SerializableFinder of; 427 428 /// Only an ancestor of [of] matching this finder will be found. 429 final SerializableFinder matching; 430 431 /// Whether the widget matching [of] will be considered for a match. 432 final bool matchRoot; 433 434 @override 435 String get finderType => 'Ancestor'; 436 437 @override 438 Map<String, String> serialize() { 439 return super.serialize() 440 ..addAll(of.serialize().map((String key, String value) => MapEntry<String, String>('of_$key', value))) 441 ..addAll(matching.serialize().map((String key, String value) => MapEntry<String, String>('matching_$key', value))) 442 ..addAll(<String, String>{ 443 'matchRoot': matchRoot ? 'true' : 'false', 444 }); 445 } 446 447 /// Deserializes the finder from JSON generated by [serialize]. 448 static Ancestor deserialize(Map<String, String> json) { 449 final Map<String, String> of = <String, String>{}; 450 final Map<String, String> matching = <String, String>{}; 451 final Map<String, String> other = <String, String>{}; 452 for (String key in json.keys) { 453 if (key.startsWith('of_')) { 454 of[key.substring('of_'.length)] = json[key]; 455 } else if (key.startsWith('matching_')) { 456 matching[key.substring('matching_'.length)] = json[key]; 457 } else { 458 other[key] = json[key]; 459 } 460 } 461 return Ancestor( 462 of: SerializableFinder.deserialize(of), 463 matching: SerializableFinder.deserialize(matching), 464 matchRoot: other['matchRoot'] == 'true', 465 ); 466 } 467} 468 469/// A Flutter driver command that retrieves a semantics id using a specified finder. 470/// 471/// This command requires assertions to be enabled on the device. 472/// 473/// If the object returned by the finder does not have its own semantics node, 474/// then the semantics node of the first ancestor is returned instead. 475/// 476/// Throws an error if a finder returns multiple objects or if there are no 477/// semantics nodes. 478/// 479/// Semantics must be enabled to use this method, either using a platform 480/// specific shell command or [FlutterDriver.setSemantics]. 481class GetSemanticsId extends CommandWithTarget { 482 483 /// Creates a command which finds a Widget and then looks up the semantic id. 484 GetSemanticsId(SerializableFinder finder, {Duration timeout}) : super(finder, timeout: timeout); 485 486 /// Creates a command from a json map. 487 GetSemanticsId.deserialize(Map<String, String> json) 488 : super.deserialize(json); 489 490 @override 491 String get kind => 'get_semantics_id'; 492} 493 494/// The result of a [GetSemanticsId] command. 495class GetSemanticsIdResult extends Result { 496 497 /// Creates a new [GetSemanticsId] result. 498 const GetSemanticsIdResult(this.id); 499 500 /// The semantics id of the node; 501 final int id; 502 503 /// Deserializes this result from JSON. 504 static GetSemanticsIdResult fromJson(Map<String, dynamic> json) { 505 return GetSemanticsIdResult(json['id']); 506 } 507 508 @override 509 Map<String, dynamic> toJson() => <String, dynamic>{'id': id}; 510} 511