extension.dart 16.1 KB
Newer Older
1 2 3 4 5
// 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 'dart:async';
6

7 8
import 'package:meta/meta.dart';

9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/gestures.dart';
11
import 'package:flutter/material.dart';
12
import 'package:flutter/rendering.dart' show RendererBinding, SemanticsHandle;
13
import 'package:flutter/scheduler.dart';
14
import 'package:flutter/services.dart';
15
import 'package:flutter/widgets.dart';
16
import 'package:flutter_test/flutter_test.dart';
17

18 19 20 21 22 23 24 25 26
import '../common/error.dart';
import '../common/find.dart';
import '../common/frame_sync.dart';
import '../common/gesture.dart';
import '../common/health.dart';
import '../common/message.dart';
import '../common/render_tree.dart';
import '../common/request_data.dart';
import '../common/semantics.dart';
27
import '../common/text.dart';
28

29 30
const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
31

32 33 34 35
/// Signature for the handler passed to [enableFlutterDriverExtension].
///
/// Messages are described in string form and should return a [Future] which
/// eventually completes to a string response.
36 37
typedef Future<String> DataHandler(String message);

38
class _DriverBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, PaintingBinding, RendererBinding, WidgetsBinding {
39 40 41 42
  _DriverBinding(this._handler);

  final DataHandler _handler;

43 44 45
  @override
  void initServiceExtensions() {
    super.initServiceExtensions();
46
    final FlutterDriverExtension extension = new FlutterDriverExtension(_handler);
47 48
    registerServiceExtension(
      name: _extensionMethodName,
49
      callback: extension.call,
50 51 52
    );
  }
}
53 54 55 56 57 58 59 60

/// Enables Flutter Driver VM service extension.
///
/// This extension is required for tests that use `package:flutter_driver` to
/// drive applications from a separate process.
///
/// Call this function prior to running your application, e.g. before you call
/// `runApp`.
61 62 63 64
///
/// Optionally you can pass a [DataHandler] callback. It will be called if the
/// test calls [FlutterDriver.requestData].
void enableFlutterDriverExtension({ DataHandler handler }) {
65
  assert(WidgetsBinding.instance == null);
66
  new _DriverBinding(handler);
67
  assert(WidgetsBinding.instance is _DriverBinding);
68 69
}

70
/// Signature for functions that handle a command and return a result.
71
typedef Future<Result> CommandHandlerCallback(Command c);
72

73
/// Signature for functions that deserialize a JSON map to a command object.
74 75
typedef Command CommandDeserializerCallback(Map<String, String> params);

76 77
/// Signature for functions that run the given finder and return the [Element]
/// found, if any, or null otherwise.
78
typedef Finder FinderConstructor(SerializableFinder finder);
79

80 81 82 83 84
/// The class that manages communication between a Flutter Driver test and the
/// application being remote-controlled, on the application side.
///
/// This is not normally used directly. It is instantiated automatically when
/// calling [enableFlutterDriverExtension].
85 86
@visibleForTesting
class FlutterDriverExtension {
87 88
  final TestTextInput _testTextInput = new TestTextInput();

89
  /// Creates an object to manage a Flutter Driver connection.
90
  FlutterDriverExtension(this._requestDataHandler) {
91 92
    _testTextInput.register();

93
    _commandHandlers.addAll(<String, CommandHandlerCallback>{
94
      'get_health': _getHealth,
95
      'get_render_tree': _getRenderTree,
96
      'enter_text': _enterText,
97
      'get_text': _getText,
98
      'request_data': _requestData,
99 100
      'scroll': _scroll,
      'scrollIntoView': _scrollIntoView,
101 102
      'set_frame_sync': _setFrameSync,
      'set_semantics': _setSemantics,
103
      'set_text_entry_emulation': _setTextEntryEmulation,
104
      'tap': _tap,
105
      'waitFor': _waitFor,
106
      'waitForAbsent': _waitForAbsent,
107
      'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
108
    });
109

110
    _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
111 112
      'get_health': (Map<String, String> params) => new GetHealth.deserialize(params),
      'get_render_tree': (Map<String, String> params) => new GetRenderTree.deserialize(params),
113
      'enter_text': (Map<String, String> params) => new EnterText.deserialize(params),
114
      'get_text': (Map<String, String> params) => new GetText.deserialize(params),
115
      'request_data': (Map<String, String> params) => new RequestData.deserialize(params),
116 117
      'scroll': (Map<String, String> params) => new Scroll.deserialize(params),
      'scrollIntoView': (Map<String, String> params) => new ScrollIntoView.deserialize(params),
118 119
      'set_frame_sync': (Map<String, String> params) => new SetFrameSync.deserialize(params),
      'set_semantics': (Map<String, String> params) => new SetSemantics.deserialize(params),
120
      'set_text_entry_emulation': (Map<String, String> params) => new SetTextEntryEmulation.deserialize(params),
121
      'tap': (Map<String, String> params) => new Tap.deserialize(params),
122
      'waitFor': (Map<String, String> params) => new WaitFor.deserialize(params),
123
      'waitForAbsent': (Map<String, String> params) => new WaitForAbsent.deserialize(params),
124
      'waitUntilNoTransientCallbacks': (Map<String, String> params) => new WaitUntilNoTransientCallbacks.deserialize(params),
125
    });
126

127
    _finders.addAll(<String, FinderConstructor>{
128 129 130 131
      'ByText': (SerializableFinder finder) => _createByTextFinder(finder),
      'ByTooltipMessage': (SerializableFinder finder) => _createByTooltipMessageFinder(finder),
      'ByValueKey': (SerializableFinder finder) => _createByValueKeyFinder(finder),
      'ByType': (SerializableFinder finder) => _createByTypeFinder(finder),
132
    });
133 134
  }

135 136 137 138
  final DataHandler _requestDataHandler;

  static final Logger _log = new Logger('FlutterDriverExtension');

139
  final WidgetController _prober = new WidgetController(WidgetsBinding.instance);
140 141
  final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
  final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
142
  final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
143

144 145 146 147
  /// With [_frameSync] enabled, Flutter Driver will wait to perform an action
  /// until there are no pending frames in the app under test.
  bool _frameSync = true;

148 149 150 151 152 153 154 155 156 157
  /// Processes a driver command configured by [params] and returns a result
  /// as an arbitrary JSON object.
  ///
  /// [params] must contain key "command" whose value is a string that
  /// identifies the kind of the command and its corresponding
  /// [CommandDeserializerCallback]. Other keys and values are specific to the
  /// concrete implementation of [Command] and [CommandDeserializerCallback].
  ///
  /// The returned JSON is command specific. Generally the caller deserializes
  /// the result into a subclass of [Result], but that's not strictly required.
158
  @visibleForTesting
159
  Future<Map<String, dynamic>> call(Map<String, String> params) async {
160
    final String commandKind = params['command'];
161
    try {
162 163
      final CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
      final CommandDeserializerCallback commandDeserializer =
164
          _commandDeserializers[commandKind];
165 166
      if (commandHandler == null || commandDeserializer == null)
        throw 'Extension $_extensionMethod does not support command $commandKind';
167 168
      final Command command = commandDeserializer(params);
      final Result response = await commandHandler(command).timeout(command.timeout);
169
      return _makeResponse(response?.toJson());
170
    } on TimeoutException catch (error, stackTrace) {
171
      final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace';
172
      _log.error(msg);
173
      return _makeResponse(msg, isError: true);
174
    } catch (error, stackTrace) {
175
      final String msg = 'Uncaught extension error while executing $commandKind: $error\n$stackTrace';
176 177
      _log.error(msg);
      return _makeResponse(msg, isError: true);
178 179 180
    }
  }

181 182 183 184 185 186 187
  Map<String, dynamic> _makeResponse(dynamic response, {bool isError: false}) {
    return <String, dynamic>{
      'isError': isError,
      'response': response,
    };
  }

188
  Future<Health> _getHealth(Command command) async => new Health(HealthStatus.ok);
189

190 191 192 193
  Future<RenderTree> _getRenderTree(Command command) async {
    return new RenderTree(RendererBinding.instance?.renderView?.toStringDeep());
  }

194 195 196 197 198 199 200 201 202 203 204 205 206
  // Waits until at the end of a frame the provided [condition] is [true].
  Future<Null> _waitUntilFrame(bool condition(), [Completer<Null> completer]) {
    completer ??= new Completer<Null>();
    if (!condition()) {
      SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
        _waitUntilFrame(condition, completer);
      });
    } else {
      completer.complete();
    }
    return completer.future;
  }

207
  /// Runs `finder` repeatedly until it finds one or more [Element]s.
208
  Future<Finder> _waitForElement(Finder finder) async {
209 210 211
    // TODO(mravn): This method depends on async execution. A refactoring
    // for sync-async semantics is tracked in https://github.com/flutter/flutter/issues/16801.
    await new Future<void>.value(null);
212 213
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
214

215
    await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
216 217 218 219 220

    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

    return finder;
221 222
  }

223 224 225 226 227
  /// Runs `finder` repeatedly until it finds zero [Element]s.
  Future<Finder> _waitForAbsentElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

228
    await _waitUntilFrame(() => finder.evaluate().isEmpty);
229 230 231 232 233 234 235

    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

    return finder;
  }

236 237
  Finder _createByTextFinder(ByText arguments) {
    return find.text(arguments.text);
238 239
  }

240 241
  Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
    return find.byElementPredicate((Element element) {
242
      final Widget widget = element.widget;
243 244 245
      if (widget is Tooltip)
        return widget.message == arguments.text;
      return false;
246
    }, description: 'widget with text tooltip "${arguments.text}"');
247 248
  }

249
  Finder _createByValueKeyFinder(ByValueKey arguments) {
250 251 252 253 254 255 256 257
    switch (arguments.keyValueType) {
      case 'int':
        return find.byKey(new ValueKey<int>(arguments.keyValue));
      case 'String':
        return find.byKey(new ValueKey<String>(arguments.keyValue));
      default:
        throw 'Unsupported ByValueKey type: ${arguments.keyValueType}';
    }
258 259
  }

260 261 262 263 264 265
  Finder _createByTypeFinder(ByType arguments) {
    return find.byElementPredicate((Element element) {
      return element.widget.runtimeType.toString() == arguments.type;
    }, description: 'widget with runtimeType "${arguments.type}"');
  }

266
  Finder _createFinder(SerializableFinder finder) {
267
    final FinderConstructor constructor = _finders[finder.finderType];
268

269
    if (constructor == null)
270 271
      throw 'Unsupported finder type: ${finder.finderType}';

272
    return constructor(finder);
273 274
  }

275
  Future<TapResult> _tap(Command command) async {
276
    final Tap tapCommand = command;
277 278 279 280
    final Finder computedFinder = await _waitForElement(
      _createFinder(tapCommand.finder).hitTestable()
    );
    await _prober.tap(computedFinder);
281 282 283
    return new TapResult();
  }

284
  Future<WaitForResult> _waitFor(Command command) async {
285
    final WaitFor waitForCommand = command;
286 287
    await _waitForElement(_createFinder(waitForCommand.finder));
    return new WaitForResult();
288 289
  }

290 291 292 293 294 295
  Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
    final WaitForAbsent waitForAbsentCommand = command;
    await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder));
    return new WaitForAbsentResult();
  }

296 297 298 299 300
  Future<Null> _waitUntilNoTransientCallbacks(Command command) async {
    if (SchedulerBinding.instance.transientCallbackCount != 0)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
  }

301
  Future<ScrollResult> _scroll(Command command) async {
302 303
    final Scroll scrollCommand = command;
    final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
304
    final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond;
305 306
    final Offset delta = new Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
    final Duration pause = scrollCommand.duration ~/ totalMoves;
307 308
    final Offset startLocation = _prober.getCenter(target);
    Offset currentLocation = startLocation;
309 310
    final TestPointer pointer = new TestPointer(1);
    final HitTestResult hitTest = new HitTestResult();
311

312 313
    _prober.binding.hitTest(hitTest, startLocation);
    _prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
314 315
    await new Future<Null>.value(); // so that down and move don't happen in the same microtask
    for (int moves = 0; moves < totalMoves; moves += 1) {
316
      currentLocation = currentLocation + delta;
317
      _prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
318 319
      await new Future<Null>.delayed(pause);
    }
320
    _prober.binding.dispatchEvent(pointer.up(), hitTest);
321 322 323 324

    return new ScrollResult();
  }

325
  Future<ScrollResult> _scrollIntoView(Command command) async {
326 327
    final ScrollIntoView scrollIntoViewCommand = command;
    final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
Adam Barth's avatar
Adam Barth committed
328
    await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0);
329 330 331
    return new ScrollResult();
  }

332
  Future<GetTextResult> _getText(Command command) async {
333 334
    final GetText getTextCommand = command;
    final Finder target = await _waitForElement(_createFinder(getTextCommand.finder));
335
    // TODO(yjbanov): support more ways to read text
336
    final Text text = target.evaluate().single.widget;
337 338
    return new GetTextResult(text.data);
  }
339

340 341 342 343 344 345 346 347 348 349
  Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
    final SetTextEntryEmulation setTextEntryEmulationCommand = command;
    if (setTextEntryEmulationCommand.enabled) {
      _testTextInput.register();
    } else {
      _testTextInput.unregister();
    }
    return new SetTextEntryEmulationResult();
  }

350
  Future<EnterTextResult> _enterText(Command command) async {
351 352 353 354
    if (!_testTextInput.isRegistered) {
      throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is '
            'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.';
    }
355 356 357 358 359
    final EnterText enterTextCommand = command;
    _testTextInput.enterText(enterTextCommand.text);
    return new EnterTextResult();
  }

360 361
  Future<RequestDataResult> _requestData(Command command) async {
    final RequestData requestDataCommand = command;
362
    return new RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message));
363 364
  }

365
  Future<SetFrameSyncResult> _setFrameSync(Command command) async {
366
    final SetFrameSync setFrameSyncCommand = command;
367 368 369
    _frameSync = setFrameSyncCommand.enabled;
    return new SetFrameSyncResult();
  }
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392

  SemanticsHandle _semantics;
  bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null;

  Future<SetSemanticsResult> _setSemantics(Command command) async {
    final SetSemantics setSemanticsCommand = command;
    final bool semanticsWasEnabled = _semanticsIsEnabled;
    if (setSemanticsCommand.enabled && _semantics == null) {
      _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics();
      if (!semanticsWasEnabled) {
        // wait for the first frame where semantics is enabled.
        final Completer<Null> completer = new Completer<Null>();
        SchedulerBinding.instance.addPostFrameCallback((Duration d) {
          completer.complete();
        });
        await completer.future;
      }
    } else if (!setSemanticsCommand.enabled && _semantics != null) {
      _semantics.dispose();
      _semantics = null;
    }
    return new SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled);
  }
393
}