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 'dart:async'; 6 7import 'package:flutter/semantics.dart'; 8import 'package:meta/meta.dart'; 9 10import 'package:flutter/cupertino.dart'; 11import 'package:flutter/foundation.dart'; 12import 'package:flutter/gestures.dart'; 13import 'package:flutter/material.dart'; 14import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle; 15import 'package:flutter/scheduler.dart'; 16import 'package:flutter/services.dart'; 17import 'package:flutter/widgets.dart'; 18import 'package:flutter_test/flutter_test.dart'; 19 20import '../common/diagnostics_tree.dart'; 21import '../common/error.dart'; 22import '../common/find.dart'; 23import '../common/frame_sync.dart'; 24import '../common/geometry.dart'; 25import '../common/gesture.dart'; 26import '../common/health.dart'; 27import '../common/message.dart'; 28import '../common/render_tree.dart'; 29import '../common/request_data.dart'; 30import '../common/semantics.dart'; 31import '../common/text.dart'; 32 33const String _extensionMethodName = 'driver'; 34const String _extensionMethod = 'ext.flutter.$_extensionMethodName'; 35 36/// Signature for the handler passed to [enableFlutterDriverExtension]. 37/// 38/// Messages are described in string form and should return a [Future] which 39/// eventually completes to a string response. 40typedef DataHandler = Future<String> Function(String message); 41 42class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding { 43 _DriverBinding(this._handler, this._silenceErrors); 44 45 final DataHandler _handler; 46 final bool _silenceErrors; 47 48 @override 49 void initServiceExtensions() { 50 super.initServiceExtensions(); 51 final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors); 52 registerServiceExtension( 53 name: _extensionMethodName, 54 callback: extension.call, 55 ); 56 } 57} 58 59/// Enables Flutter Driver VM service extension. 60/// 61/// This extension is required for tests that use `package:flutter_driver` to 62/// drive applications from a separate process. 63/// 64/// Call this function prior to running your application, e.g. before you call 65/// `runApp`. 66/// 67/// Optionally you can pass a [DataHandler] callback. It will be called if the 68/// test calls [FlutterDriver.requestData]. 69/// 70/// `silenceErrors` will prevent exceptions from being logged. This is useful 71/// for tests where exceptions are expected. Defaults to false. Any errors 72/// will still be returned in the `response` field of the result json along 73/// with an `isError` boolean. 74void enableFlutterDriverExtension({ DataHandler handler, bool silenceErrors = false }) { 75 assert(WidgetsBinding.instance == null); 76 _DriverBinding(handler, silenceErrors); 77 assert(WidgetsBinding.instance is _DriverBinding); 78} 79 80/// Signature for functions that handle a command and return a result. 81typedef CommandHandlerCallback = Future<Result> Function(Command c); 82 83/// Signature for functions that deserialize a JSON map to a command object. 84typedef CommandDeserializerCallback = Command Function(Map<String, String> params); 85 86/// Signature for functions that run the given finder and return the [Element] 87/// found, if any, or null otherwise. 88typedef FinderConstructor = Finder Function(SerializableFinder finder); 89 90/// The class that manages communication between a Flutter Driver test and the 91/// application being remote-controlled, on the application side. 92/// 93/// This is not normally used directly. It is instantiated automatically when 94/// calling [enableFlutterDriverExtension]. 95@visibleForTesting 96class FlutterDriverExtension { 97 /// Creates an object to manage a Flutter Driver connection. 98 FlutterDriverExtension(this._requestDataHandler, this._silenceErrors) { 99 _testTextInput.register(); 100 101 _commandHandlers.addAll(<String, CommandHandlerCallback>{ 102 'get_health': _getHealth, 103 'get_render_tree': _getRenderTree, 104 'enter_text': _enterText, 105 'get_text': _getText, 106 'request_data': _requestData, 107 'scroll': _scroll, 108 'scrollIntoView': _scrollIntoView, 109 'set_frame_sync': _setFrameSync, 110 'set_semantics': _setSemantics, 111 'set_text_entry_emulation': _setTextEntryEmulation, 112 'tap': _tap, 113 'waitFor': _waitFor, 114 'waitForAbsent': _waitForAbsent, 115 'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks, 116 'waitUntilNoPendingFrame': _waitUntilNoPendingFrame, 117 'waitUntilFirstFrameRasterized': _waitUntilFirstFrameRasterized, 118 'get_semantics_id': _getSemanticsId, 119 'get_offset': _getOffset, 120 'get_diagnostics_tree': _getDiagnosticsTree, 121 }); 122 123 _commandDeserializers.addAll(<String, CommandDeserializerCallback>{ 124 'get_health': (Map<String, String> params) => GetHealth.deserialize(params), 125 'get_render_tree': (Map<String, String> params) => GetRenderTree.deserialize(params), 126 'enter_text': (Map<String, String> params) => EnterText.deserialize(params), 127 'get_text': (Map<String, String> params) => GetText.deserialize(params), 128 'request_data': (Map<String, String> params) => RequestData.deserialize(params), 129 'scroll': (Map<String, String> params) => Scroll.deserialize(params), 130 'scrollIntoView': (Map<String, String> params) => ScrollIntoView.deserialize(params), 131 'set_frame_sync': (Map<String, String> params) => SetFrameSync.deserialize(params), 132 'set_semantics': (Map<String, String> params) => SetSemantics.deserialize(params), 133 'set_text_entry_emulation': (Map<String, String> params) => SetTextEntryEmulation.deserialize(params), 134 'tap': (Map<String, String> params) => Tap.deserialize(params), 135 'waitFor': (Map<String, String> params) => WaitFor.deserialize(params), 136 'waitForAbsent': (Map<String, String> params) => WaitForAbsent.deserialize(params), 137 'waitUntilNoTransientCallbacks': (Map<String, String> params) => WaitUntilNoTransientCallbacks.deserialize(params), 138 'waitUntilNoPendingFrame': (Map<String, String> params) => WaitUntilNoPendingFrame.deserialize(params), 139 'waitUntilFirstFrameRasterized': (Map<String, String> params) => WaitUntilFirstFrameRasterized.deserialize(params), 140 'get_semantics_id': (Map<String, String> params) => GetSemanticsId.deserialize(params), 141 'get_offset': (Map<String, String> params) => GetOffset.deserialize(params), 142 'get_diagnostics_tree': (Map<String, String> params) => GetDiagnosticsTree.deserialize(params), 143 }); 144 145 _finders.addAll(<String, FinderConstructor>{ 146 'ByText': (SerializableFinder finder) => _createByTextFinder(finder), 147 'ByTooltipMessage': (SerializableFinder finder) => _createByTooltipMessageFinder(finder), 148 'BySemanticsLabel': (SerializableFinder finder) => _createBySemanticsLabelFinder(finder), 149 'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder), 150 'ByType': (SerializableFinder finder) => _createByTypeFinder(finder), 151 'PageBack': (SerializableFinder finder) => _createPageBackFinder(), 152 'Ancestor': (SerializableFinder finder) => _createAncestorFinder(finder), 153 'Descendant': (SerializableFinder finder) => _createDescendantFinder(finder), 154 }); 155 } 156 157 final TestTextInput _testTextInput = TestTextInput(); 158 159 final DataHandler _requestDataHandler; 160 final bool _silenceErrors; 161 162 static final Logger _log = Logger('FlutterDriverExtension'); 163 164 final WidgetController _prober = LiveWidgetController(WidgetsBinding.instance); 165 final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{}; 166 final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{}; 167 final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{}; 168 169 /// With [_frameSync] enabled, Flutter Driver will wait to perform an action 170 /// until there are no pending frames in the app under test. 171 bool _frameSync = true; 172 173 /// Processes a driver command configured by [params] and returns a result 174 /// as an arbitrary JSON object. 175 /// 176 /// [params] must contain key "command" whose value is a string that 177 /// identifies the kind of the command and its corresponding 178 /// [CommandDeserializerCallback]. Other keys and values are specific to the 179 /// concrete implementation of [Command] and [CommandDeserializerCallback]. 180 /// 181 /// The returned JSON is command specific. Generally the caller deserializes 182 /// the result into a subclass of [Result], but that's not strictly required. 183 @visibleForTesting 184 Future<Map<String, dynamic>> call(Map<String, String> params) async { 185 final String commandKind = params['command']; 186 try { 187 final CommandHandlerCallback commandHandler = _commandHandlers[commandKind]; 188 final CommandDeserializerCallback commandDeserializer = 189 _commandDeserializers[commandKind]; 190 if (commandHandler == null || commandDeserializer == null) 191 throw 'Extension $_extensionMethod does not support command $commandKind'; 192 final Command command = commandDeserializer(params); 193 assert(WidgetsBinding.instance.isRootWidgetAttached || !command.requiresRootWidgetAttached, 194 'No root widget is attached; have you remembered to call runApp()?'); 195 Future<Result> responseFuture = commandHandler(command); 196 if (command.timeout != null) 197 responseFuture = responseFuture.timeout(command.timeout); 198 final Result response = await responseFuture; 199 return _makeResponse(response?.toJson()); 200 } on TimeoutException catch (error, stackTrace) { 201 final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace'; 202 _log.error(msg); 203 return _makeResponse(msg, isError: true); 204 } catch (error, stackTrace) { 205 final String msg = 'Uncaught extension error while executing $commandKind: $error\n$stackTrace'; 206 if (!_silenceErrors) 207 _log.error(msg); 208 return _makeResponse(msg, isError: true); 209 } 210 } 211 212 Map<String, dynamic> _makeResponse(dynamic response, { bool isError = false }) { 213 return <String, dynamic>{ 214 'isError': isError, 215 'response': response, 216 }; 217 } 218 219 Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok); 220 221 Future<RenderTree> _getRenderTree(Command command) async { 222 return RenderTree(RendererBinding.instance?.renderView?.toStringDeep()); 223 } 224 225 // This can be used to wait for the first frame being rasterized during app launch. 226 Future<Result> _waitUntilFirstFrameRasterized(Command command) async { 227 await WidgetsBinding.instance.waitUntilFirstFrameRasterized; 228 return null; 229 } 230 231 // Waits until at the end of a frame the provided [condition] is [true]. 232 Future<void> _waitUntilFrame(bool condition(), [ Completer<void> completer ]) { 233 completer ??= Completer<void>(); 234 if (!condition()) { 235 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { 236 _waitUntilFrame(condition, completer); 237 }); 238 } else { 239 completer.complete(); 240 } 241 return completer.future; 242 } 243 244 /// Runs `finder` repeatedly until it finds one or more [Element]s. 245 Future<Finder> _waitForElement(Finder finder) async { 246 if (_frameSync) 247 await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); 248 249 await _waitUntilFrame(() => finder.evaluate().isNotEmpty); 250 251 if (_frameSync) 252 await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); 253 254 return finder; 255 } 256 257 /// Runs `finder` repeatedly until it finds zero [Element]s. 258 Future<Finder> _waitForAbsentElement(Finder finder) async { 259 if (_frameSync) 260 await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); 261 262 await _waitUntilFrame(() => finder.evaluate().isEmpty); 263 264 if (_frameSync) 265 await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); 266 267 return finder; 268 } 269 270 Finder _createByTextFinder(ByText arguments) { 271 return find.text(arguments.text); 272 } 273 274 Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) { 275 return find.byElementPredicate((Element element) { 276 final Widget widget = element.widget; 277 if (widget is Tooltip) 278 return widget.message == arguments.text; 279 return false; 280 }, description: 'widget with text tooltip "${arguments.text}"'); 281 } 282 283 Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) { 284 return find.byElementPredicate((Element element) { 285 if (element is! RenderObjectElement) { 286 return false; 287 } 288 final String semanticsLabel = element.renderObject?.debugSemantics?.label; 289 if (semanticsLabel == null) { 290 return false; 291 } 292 final Pattern label = arguments.label; 293 return label is RegExp 294 ? label.hasMatch(semanticsLabel) 295 : label == semanticsLabel; 296 }, description: 'widget with semantic label "${arguments.label}"'); 297 } 298 299 Finder _createByValueKeyFinder(ByValueKey arguments) { 300 switch (arguments.keyValueType) { 301 case 'int': 302 return find.byKey(ValueKey<int>(arguments.keyValue)); 303 case 'String': 304 return find.byKey(ValueKey<String>(arguments.keyValue)); 305 default: 306 throw 'Unsupported ByValueKey type: ${arguments.keyValueType}'; 307 } 308 } 309 310 Finder _createByTypeFinder(ByType arguments) { 311 return find.byElementPredicate((Element element) { 312 return element.widget.runtimeType.toString() == arguments.type; 313 }, description: 'widget with runtimeType "${arguments.type}"'); 314 } 315 316 Finder _createPageBackFinder() { 317 return find.byElementPredicate((Element element) { 318 final Widget widget = element.widget; 319 if (widget is Tooltip) 320 return widget.message == 'Back'; 321 if (widget is CupertinoNavigationBarBackButton) 322 return true; 323 return false; 324 }, description: 'Material or Cupertino back button'); 325 } 326 327 Finder _createAncestorFinder(Ancestor arguments) { 328 return find.ancestor( 329 of: _createFinder(arguments.of), 330 matching: _createFinder(arguments.matching), 331 matchRoot: arguments.matchRoot, 332 ); 333 } 334 335 Finder _createDescendantFinder(Descendant arguments) { 336 return find.descendant( 337 of: _createFinder(arguments.of), 338 matching: _createFinder(arguments.matching), 339 matchRoot: arguments.matchRoot, 340 ); 341 } 342 343 Finder _createFinder(SerializableFinder finder) { 344 final FinderConstructor constructor = _finders[finder.finderType]; 345 346 if (constructor == null) 347 throw 'Unsupported finder type: ${finder.finderType}'; 348 349 return constructor(finder); 350 } 351 352 Future<TapResult> _tap(Command command) async { 353 final Tap tapCommand = command; 354 final Finder computedFinder = await _waitForElement( 355 _createFinder(tapCommand.finder).hitTestable() 356 ); 357 await _prober.tap(computedFinder); 358 return const TapResult(); 359 } 360 361 Future<WaitForResult> _waitFor(Command command) async { 362 final WaitFor waitForCommand = command; 363 await _waitForElement(_createFinder(waitForCommand.finder)); 364 return const WaitForResult(); 365 } 366 367 Future<WaitForAbsentResult> _waitForAbsent(Command command) async { 368 final WaitForAbsent waitForAbsentCommand = command; 369 await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder)); 370 return const WaitForAbsentResult(); 371 } 372 373 Future<Result> _waitUntilNoTransientCallbacks(Command command) async { 374 if (SchedulerBinding.instance.transientCallbackCount != 0) 375 await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0); 376 return null; 377 } 378 379 /// Returns a future that waits until no pending frame is scheduled (frame is synced). 380 /// 381 /// Specifically, it checks: 382 /// * Whether the count of transient callbacks is zero. 383 /// * Whether there's no pending request for scheduling a new frame. 384 /// 385 /// We consider the frame is synced when both conditions are met. 386 /// 387 /// This method relies on a Flutter Driver mechanism called "frame sync", 388 /// which waits for transient animations to finish. Persistent animations will 389 /// cause this to wait forever. 390 /// 391 /// If a test needs to interact with the app while animations are running, it 392 /// should avoid this method and instead disable the frame sync using 393 /// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more 394 /// details on how to do this. Note, disabling frame sync will require the 395 /// test author to use some other method to avoid flakiness. 396 Future<Result> _waitUntilNoPendingFrame(Command command) async { 397 await _waitUntilFrame(() { 398 return SchedulerBinding.instance.transientCallbackCount == 0 399 && !SchedulerBinding.instance.hasScheduledFrame; 400 }); 401 return null; 402 } 403 404 Future<GetSemanticsIdResult> _getSemanticsId(Command command) async { 405 final GetSemanticsId semanticsCommand = command; 406 final Finder target = await _waitForElement(_createFinder(semanticsCommand.finder)); 407 final Element element = target.evaluate().single; 408 RenderObject renderObject = element.renderObject; 409 SemanticsNode node; 410 while (renderObject != null && node == null) { 411 node = renderObject.debugSemantics; 412 renderObject = renderObject.parent; 413 } 414 if (node == null) 415 throw StateError('No semantics data found'); 416 return GetSemanticsIdResult(node.id); 417 } 418 419 Future<GetOffsetResult> _getOffset(Command command) async { 420 final GetOffset getOffsetCommand = command; 421 final Finder finder = await _waitForElement(_createFinder(getOffsetCommand.finder)); 422 final Element element = finder.evaluate().single; 423 final RenderBox box = element.renderObject; 424 Offset localPoint; 425 switch (getOffsetCommand.offsetType) { 426 case OffsetType.topLeft: 427 localPoint = Offset.zero; 428 break; 429 case OffsetType.topRight: 430 localPoint = box.size.topRight(Offset.zero); 431 break; 432 case OffsetType.bottomLeft: 433 localPoint = box.size.bottomLeft(Offset.zero); 434 break; 435 case OffsetType.bottomRight: 436 localPoint = box.size.bottomRight(Offset.zero); 437 break; 438 case OffsetType.center: 439 localPoint = box.size.center(Offset.zero); 440 break; 441 } 442 final Offset globalPoint = box.localToGlobal(localPoint); 443 return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy); 444 } 445 446 Future<DiagnosticsTreeResult> _getDiagnosticsTree(Command command) async { 447 final GetDiagnosticsTree diagnosticsCommand = command; 448 final Finder finder = await _waitForElement(_createFinder(diagnosticsCommand.finder)); 449 final Element element = finder.evaluate().single; 450 DiagnosticsNode diagnosticsNode; 451 switch (diagnosticsCommand.diagnosticsType) { 452 case DiagnosticsType.renderObject: 453 diagnosticsNode = element.renderObject.toDiagnosticsNode(); 454 break; 455 case DiagnosticsType.widget: 456 diagnosticsNode = element.toDiagnosticsNode(); 457 break; 458 } 459 return DiagnosticsTreeResult(diagnosticsNode.toJsonMap(DiagnosticsSerializationDelegate( 460 subtreeDepth: diagnosticsCommand.subtreeDepth, 461 includeProperties: diagnosticsCommand.includeProperties, 462 ))); 463 } 464 465 Future<ScrollResult> _scroll(Command command) async { 466 final Scroll scrollCommand = command; 467 final Finder target = await _waitForElement(_createFinder(scrollCommand.finder)); 468 final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond; 469 final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble(); 470 final Duration pause = scrollCommand.duration ~/ totalMoves; 471 final Offset startLocation = _prober.getCenter(target); 472 Offset currentLocation = startLocation; 473 final TestPointer pointer = TestPointer(1); 474 final HitTestResult hitTest = HitTestResult(); 475 476 _prober.binding.hitTest(hitTest, startLocation); 477 _prober.binding.dispatchEvent(pointer.down(startLocation), hitTest); 478 await Future<void>.value(); // so that down and move don't happen in the same microtask 479 for (int moves = 0; moves < totalMoves; moves += 1) { 480 currentLocation = currentLocation + delta; 481 _prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest); 482 await Future<void>.delayed(pause); 483 } 484 _prober.binding.dispatchEvent(pointer.up(), hitTest); 485 486 return const ScrollResult(); 487 } 488 489 Future<ScrollResult> _scrollIntoView(Command command) async { 490 final ScrollIntoView scrollIntoViewCommand = command; 491 final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder)); 492 await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0); 493 return const ScrollResult(); 494 } 495 496 Future<GetTextResult> _getText(Command command) async { 497 final GetText getTextCommand = command; 498 final Finder target = await _waitForElement(_createFinder(getTextCommand.finder)); 499 // TODO(yjbanov): support more ways to read text 500 final Text text = target.evaluate().single.widget; 501 return GetTextResult(text.data); 502 } 503 504 Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async { 505 final SetTextEntryEmulation setTextEntryEmulationCommand = command; 506 if (setTextEntryEmulationCommand.enabled) { 507 _testTextInput.register(); 508 } else { 509 _testTextInput.unregister(); 510 } 511 return const SetTextEntryEmulationResult(); 512 } 513 514 Future<EnterTextResult> _enterText(Command command) async { 515 if (!_testTextInput.isRegistered) { 516 throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is ' 517 'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.'; 518 } 519 final EnterText enterTextCommand = command; 520 _testTextInput.enterText(enterTextCommand.text); 521 return const EnterTextResult(); 522 } 523 524 Future<RequestDataResult> _requestData(Command command) async { 525 final RequestData requestDataCommand = command; 526 return RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message)); 527 } 528 529 Future<SetFrameSyncResult> _setFrameSync(Command command) async { 530 final SetFrameSync setFrameSyncCommand = command; 531 _frameSync = setFrameSyncCommand.enabled; 532 return const SetFrameSyncResult(); 533 } 534 535 SemanticsHandle _semantics; 536 bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null; 537 538 Future<SetSemanticsResult> _setSemantics(Command command) async { 539 final SetSemantics setSemanticsCommand = command; 540 final bool semanticsWasEnabled = _semanticsIsEnabled; 541 if (setSemanticsCommand.enabled && _semantics == null) { 542 _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); 543 if (!semanticsWasEnabled) { 544 // wait for the first frame where semantics is enabled. 545 final Completer<void> completer = Completer<void>(); 546 SchedulerBinding.instance.addPostFrameCallback((Duration d) { 547 completer.complete(); 548 }); 549 await completer.future; 550 } 551 } else if (!setSemanticsCommand.enabled && _semantics != null) { 552 _semantics.dispose(); 553 _semantics = null; 554 } 555 return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled); 556 } 557} 558