extension.dart 14.5 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

import 'error.dart';
import 'find.dart';
20
import 'frame_sync.dart';
21 22 23
import 'gesture.dart';
import 'health.dart';
import 'message.dart';
24
import 'render_tree.dart';
25
import 'request_data.dart';
26
import 'semantics.dart';
27

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

31 32 33 34
/// 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.
35 36 37 38 39 40 41
typedef Future<String> DataHandler(String message);

class _DriverBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding {
  _DriverBinding(this._handler);

  final DataHandler _handler;

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

/// 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`.
60 61 62 63
///
/// Optionally you can pass a [DataHandler] callback. It will be called if the
/// test calls [FlutterDriver.requestData].
void enableFlutterDriverExtension({ DataHandler handler }) {
64
  assert(WidgetsBinding.instance == null);
65
  new _DriverBinding(handler);
66
  assert(WidgetsBinding.instance is _DriverBinding);
67 68
}

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

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

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

79 80 81 82 83
/// 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].
84 85
@visibleForTesting
class FlutterDriverExtension {
86
  /// Creates an object to manage a Flutter Driver connection.
87
  FlutterDriverExtension(this._requestDataHandler) {
88
    _commandHandlers.addAll(<String, CommandHandlerCallback>{
89
      'get_health': _getHealth,
90
      'get_render_tree': _getRenderTree,
91
      'get_text': _getText,
92
      'request_data': _requestData,
93 94
      'scroll': _scroll,
      'scrollIntoView': _scrollIntoView,
95 96 97
      'set_frame_sync': _setFrameSync,
      'set_semantics': _setSemantics,
      'tap': _tap,
98
      'waitFor': _waitFor,
99
      'waitForAbsent': _waitForAbsent,
100
      'waitUntilNoTransientCallbacks': _waitUntilNoTransientCallbacks,
101
    });
102

103
    _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
104 105 106
      'get_health': (Map<String, String> params) => new GetHealth.deserialize(params),
      'get_render_tree': (Map<String, String> params) => new GetRenderTree.deserialize(params),
      'get_text': (Map<String, String> params) => new GetText.deserialize(params),
107
      'request_data': (Map<String, String> params) => new RequestData.deserialize(params),
108 109
      'scroll': (Map<String, String> params) => new Scroll.deserialize(params),
      'scrollIntoView': (Map<String, String> params) => new ScrollIntoView.deserialize(params),
110 111 112
      'set_frame_sync': (Map<String, String> params) => new SetFrameSync.deserialize(params),
      'set_semantics': (Map<String, String> params) => new SetSemantics.deserialize(params),
      'tap': (Map<String, String> params) => new Tap.deserialize(params),
113
      'waitFor': (Map<String, String> params) => new WaitFor.deserialize(params),
114
      'waitForAbsent': (Map<String, String> params) => new WaitForAbsent.deserialize(params),
115
      'waitUntilNoTransientCallbacks': (Map<String, String> params) => new WaitUntilNoTransientCallbacks.deserialize(params),
116
    });
117

118 119 120 121
    _finders.addAll(<String, FinderConstructor>{
      'ByText': _createByTextFinder,
      'ByTooltipMessage': _createByTooltipMessageFinder,
      'ByValueKey': _createByValueKeyFinder,
122
      'ByType': _createByTypeFinder,
123
    });
124 125
  }

126 127 128 129
  final DataHandler _requestDataHandler;

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

130
  final WidgetController _prober = new WidgetController(WidgetsBinding.instance);
131 132
  final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
  final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
133
  final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
134

135 136 137 138
  /// 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;

139 140 141 142 143 144 145 146 147 148
  /// 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.
149
  @visibleForTesting
150
  Future<Map<String, dynamic>> call(Map<String, String> params) async {
151
    final String commandKind = params['command'];
152
    try {
153 154
      final CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
      final CommandDeserializerCallback commandDeserializer =
155
          _commandDeserializers[commandKind];
156 157
      if (commandHandler == null || commandDeserializer == null)
        throw 'Extension $_extensionMethod does not support command $commandKind';
158 159
      final Command command = commandDeserializer(params);
      final Result response = await commandHandler(command).timeout(command.timeout);
160
      return _makeResponse(response?.toJson());
161
    } on TimeoutException catch (error, stackTrace) {
162
      final String msg = 'Timeout while executing $commandKind: $error\n$stackTrace';
163
      _log.error(msg);
164
      return _makeResponse(msg, isError: true);
165
    } catch (error, stackTrace) {
166
      final String msg = 'Uncaught extension error while executing $commandKind: $error\n$stackTrace';
167 168
      _log.error(msg);
      return _makeResponse(msg, isError: true);
169 170 171
    }
  }

172 173 174 175 176 177 178
  Map<String, dynamic> _makeResponse(dynamic response, {bool isError: false}) {
    return <String, dynamic>{
      'isError': isError,
      'response': response,
    };
  }

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

181 182 183 184
  Future<RenderTree> _getRenderTree(Command command) async {
    return new RenderTree(RendererBinding.instance?.renderView?.toStringDeep());
  }

185 186 187 188 189 190 191 192 193 194 195 196 197
  // 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;
  }

198
  /// Runs `finder` repeatedly until it finds one or more [Element]s.
199 200 201
  Future<Finder> _waitForElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
202

203
    await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
204 205 206 207 208

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

    return finder;
209 210
  }

211 212 213 214 215
  /// Runs `finder` repeatedly until it finds zero [Element]s.
  Future<Finder> _waitForAbsentElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);

216
    await _waitUntilFrame(() => finder.evaluate().isEmpty);
217 218 219 220 221 222 223

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

    return finder;
  }

224 225
  Finder _createByTextFinder(ByText arguments) {
    return find.text(arguments.text);
226 227
  }

228 229
  Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
    return find.byElementPredicate((Element element) {
230
      final Widget widget = element.widget;
231 232 233
      if (widget is Tooltip)
        return widget.message == arguments.text;
      return false;
234
    }, description: 'widget with text tooltip "${arguments.text}"');
235 236
  }

237
  Finder _createByValueKeyFinder(ByValueKey arguments) {
238 239 240 241 242 243 244 245
    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}';
    }
246 247
  }

248 249 250 251 252 253
  Finder _createByTypeFinder(ByType arguments) {
    return find.byElementPredicate((Element element) {
      return element.widget.runtimeType.toString() == arguments.type;
    }, description: 'widget with runtimeType "${arguments.type}"');
  }

254
  Finder _createFinder(SerializableFinder finder) {
255
    final FinderConstructor constructor = _finders[finder.finderType];
256

257
    if (constructor == null)
258 259
      throw 'Unsupported finder type: ${finder.finderType}';

260
    return constructor(finder);
261 262
  }

263
  Future<TapResult> _tap(Command command) async {
264
    final Tap tapCommand = command;
265 266 267 268
    final Finder computedFinder = await _waitForElement(
      _createFinder(tapCommand.finder).hitTestable()
    );
    await _prober.tap(computedFinder);
269 270 271
    return new TapResult();
  }

272
  Future<WaitForResult> _waitFor(Command command) async {
273
    final WaitFor waitForCommand = command;
274 275
    await _waitForElement(_createFinder(waitForCommand.finder));
    return new WaitForResult();
276 277
  }

278 279 280 281 282 283
  Future<WaitForAbsentResult> _waitForAbsent(Command command) async {
    final WaitForAbsent waitForAbsentCommand = command;
    await _waitForAbsentElement(_createFinder(waitForAbsentCommand.finder));
    return new WaitForAbsentResult();
  }

284 285 286 287 288
  Future<Null> _waitUntilNoTransientCallbacks(Command command) async {
    if (SchedulerBinding.instance.transientCallbackCount != 0)
      await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
  }

289
  Future<ScrollResult> _scroll(Command command) async {
290 291
    final Scroll scrollCommand = command;
    final Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
292
    final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.MICROSECONDS_PER_SECOND;
293 294
    final Offset delta = new Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
    final Duration pause = scrollCommand.duration ~/ totalMoves;
295 296
    final Offset startLocation = _prober.getCenter(target);
    Offset currentLocation = startLocation;
297 298
    final TestPointer pointer = new TestPointer(1);
    final HitTestResult hitTest = new HitTestResult();
299

300 301
    _prober.binding.hitTest(hitTest, startLocation);
    _prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
302 303
    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) {
304
      currentLocation = currentLocation + delta;
305
      _prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
306 307
      await new Future<Null>.delayed(pause);
    }
308
    _prober.binding.dispatchEvent(pointer.up(), hitTest);
309 310 311 312

    return new ScrollResult();
  }

313
  Future<ScrollResult> _scrollIntoView(Command command) async {
314 315
    final ScrollIntoView scrollIntoViewCommand = command;
    final Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
Adam Barth's avatar
Adam Barth committed
316
    await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment ?? 0.0);
317 318 319
    return new ScrollResult();
  }

320
  Future<GetTextResult> _getText(Command command) async {
321 322
    final GetText getTextCommand = command;
    final Finder target = await _waitForElement(_createFinder(getTextCommand.finder));
323
    // TODO(yjbanov): support more ways to read text
324
    final Text text = target.evaluate().single.widget;
325 326
    return new GetTextResult(text.data);
  }
327

328 329
  Future<RequestDataResult> _requestData(Command command) async {
    final RequestData requestDataCommand = command;
330
    return new RequestDataResult(_requestDataHandler == null ? 'No requestData Extension registered' : await _requestDataHandler(requestDataCommand.message));
331 332
  }

333
  Future<SetFrameSyncResult> _setFrameSync(Command command) async {
334
    final SetFrameSync setFrameSyncCommand = command;
335 336 337
    _frameSync = setFrameSyncCommand.enabled;
    return new SetFrameSyncResult();
  }
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

  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);
  }
361
}