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

import 'package:flutter/gestures.dart';
8
import 'package:flutter/material.dart';
9
import 'package:flutter/scheduler.dart';
10
import 'package:flutter/widgets.dart';
11
import 'package:flutter_test/flutter_test.dart';
12 13 14 15 16

import 'error.dart';
import 'find.dart';
import 'gesture.dart';
import 'health.dart';
17
import 'input.dart';
18 19
import 'message.dart';

20 21
const String _extensionMethodName = 'driver';
const String _extensionMethod = 'ext.flutter.$_extensionMethodName';
22
const Duration _kDefaultTimeout = const Duration(seconds: 5);
23

24
class _DriverBinding extends WidgetsFlutterBinding { // TODO(ianh): refactor so we're not extending a concrete binding
25 26 27 28 29 30 31 32 33 34
  @override
  void initServiceExtensions() {
    super.initServiceExtensions();
    FlutterDriverExtension extension = new FlutterDriverExtension();
    registerServiceExtension(
      name: _extensionMethodName,
      callback: extension.call
    );
  }
}
35 36 37 38 39 40 41 42 43

/// 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`.
void enableFlutterDriverExtension() {
44
  assert(WidgetsBinding.instance == null);
45
  new _DriverBinding();
46
  assert(WidgetsBinding.instance is _DriverBinding);
47 48 49
}

/// Handles a command and returns a result.
50
typedef Future<Result> CommandHandlerCallback(Command c);
51 52 53 54

/// Deserializes JSON map to a command object.
typedef Command CommandDeserializerCallback(Map<String, String> params);

55
/// Runs the finder and returns the [Element] found, or `null`.
56
typedef Finder FinderConstructor(SerializableFinder finder);
57

58 59 60 61
class FlutterDriverExtension {
  static final Logger _log = new Logger('FlutterDriverExtension');

  FlutterDriverExtension() {
62
    _commandHandlers.addAll(<String, CommandHandlerCallback>{
63 64 65 66 67 68 69 70
      'get_health': _getHealth,
      'tap': _tap,
      'get_text': _getText,
      'scroll': _scroll,
      'scrollIntoView': _scrollIntoView,
      'setInputText': _setInputText,
      'submitInputText': _submitInputText,
      'waitFor': _waitFor,
71
    });
72

73
    _commandDeserializers.addAll(<String, CommandDeserializerCallback>{
74 75 76 77
      'get_health': GetHealth.deserialize,
      'tap': Tap.deserialize,
      'get_text': GetText.deserialize,
      'scroll': Scroll.deserialize,
78
      'scrollIntoView': ScrollIntoView.deserialize,
79 80
      'setInputText': SetInputText.deserialize,
      'submitInputText': SubmitInputText.deserialize,
81 82
      'waitFor': WaitFor.deserialize,
    });
83

84 85 86 87
    _finders.addAll(<String, FinderConstructor>{
      'ByText': _createByTextFinder,
      'ByTooltipMessage': _createByTooltipMessageFinder,
      'ByValueKey': _createByValueKeyFinder,
88
    });
89 90
  }

91
  final WidgetController prober = new WidgetController(WidgetsBinding.instance);
92 93
  final Map<String, CommandHandlerCallback> _commandHandlers = <String, CommandHandlerCallback>{};
  final Map<String, CommandDeserializerCallback> _commandDeserializers = <String, CommandDeserializerCallback>{};
94
  final Map<String, FinderConstructor> _finders = <String, FinderConstructor>{};
95

96
  Future<Map<String, dynamic>> call(Map<String, String> params) async {
97
    try {
98
      String commandKind = params['command'];
99 100 101
      CommandHandlerCallback commandHandler = _commandHandlers[commandKind];
      CommandDeserializerCallback commandDeserializer =
          _commandDeserializers[commandKind];
102 103
      if (commandHandler == null || commandDeserializer == null)
        throw 'Extension $_extensionMethod does not support command $commandKind';
104
      Command command = commandDeserializer(params);
105 106 107 108
      return (await commandHandler(command)).toJson();
    } catch (error, stackTrace) {
      _log.error('Uncaught extension error: $error\n$stackTrace');
      rethrow;
109 110 111
    }
  }

112 113 114 115 116 117
  Stream<Duration> _onFrameReadyStream;
  Stream<Duration> get _onFrameReady {
    if (_onFrameReadyStream == null) {
      // Lazy-initialize the frame callback because the renderer is not yet
      // available at the time the extension is registered.
      StreamController<Duration> frameReadyController = new StreamController<Duration>.broadcast(sync: true);
118
      SchedulerBinding.instance.addPersistentFrameCallback((Duration timestamp) {
119 120 121 122 123 124 125
        frameReadyController.add(timestamp);
      });
      _onFrameReadyStream = frameReadyController.stream;
    }
    return _onFrameReadyStream;
  }

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

128 129 130 131
  /// Runs `finder` repeatedly until it finds one or more [Element]s, or times out.
  ///
  /// The timeout is five seconds.
  Future<Finder> _waitForElement(Finder finder) {
132
    // Short-circuit if the element is already on the UI
133 134
    if (finder.precache())
      return new Future<Finder>.value(finder);
135 136

    // No element yet, so we retry on frames rendered in the future.
137
    Completer<Finder> completer = new Completer<Finder>();
138 139 140 141
    StreamSubscription<Duration> subscription;

    Timer timeout = new Timer(_kDefaultTimeout, () {
      subscription.cancel();
142
      completer.completeError('Timed out waiting for ${finder.description}');
143 144 145
    });

    subscription = _onFrameReady.listen((Duration duration) {
146
      if (finder.precache()) {
147 148
        subscription.cancel();
        timeout.cancel();
149
        completer.complete(finder);
150
      }
151
    });
152 153

    return completer.future;
154 155
  }

156 157
  Finder _createByTextFinder(ByText arguments) {
    return find.text(arguments.text);
158 159
  }

160 161 162 163 164 165
  Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
    return find.byElementPredicate((Element element) {
      Widget widget = element.widget;
      if (widget is Tooltip)
        return widget.message == arguments.text;
      return false;
166
    }, description: 'widget with text tooltip "${arguments.text}"');
167 168
  }

169 170
  Finder _createByValueKeyFinder(ByValueKey arguments) {
    return find.byKey(new ValueKey<dynamic>(arguments.keyValue));
171 172
  }

173 174
  Finder _createFinder(SerializableFinder finder) {
    FinderConstructor constructor = _finders[finder.finderType];
175

176
    if (constructor == null)
177 178
      throw 'Unsupported finder type: ${finder.finderType}';

179
    return constructor(finder);
180 181
  }

182 183
  Future<TapResult> _tap(Command command) async {
    Tap tapCommand = command;
184
    await prober.tap(await _waitForElement(_createFinder(tapCommand.finder)));
185 186 187
    return new TapResult();
  }

188 189 190
  Future<WaitForResult> _waitFor(Command command) async {
    WaitFor waitForCommand = command;
    if ((await _waitForElement(_createFinder(waitForCommand.finder))).evaluate().isNotEmpty)
191 192 193 194 195
      return new WaitForResult();
    else
      return null;
  }

196 197 198 199 200 201
  Future<ScrollResult> _scroll(Command command) async {
    Scroll scrollCommand = command;
    Finder target = await _waitForElement(_createFinder(scrollCommand.finder));
    final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.MICROSECONDS_PER_SECOND;
    Offset delta = new Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
    Duration pause = scrollCommand.duration ~/ totalMoves;
202 203 204 205 206 207
    Point startLocation = prober.getCenter(target);
    Point currentLocation = startLocation;
    TestPointer pointer = new TestPointer(1);
    HitTestResult hitTest = new HitTestResult();

    prober.binding.hitTest(hitTest, startLocation);
208
    prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
209 210 211
    await new Future<Null>.value();  // so that down and move don't happen in the same microtask
    for (int moves = 0; moves < totalMoves; moves++) {
      currentLocation = currentLocation + delta;
212
      prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
213 214
      await new Future<Null>.delayed(pause);
    }
215
    prober.binding.dispatchEvent(pointer.up(), hitTest);
216 217 218 219

    return new ScrollResult();
  }

220 221 222
  Future<ScrollResult> _scrollIntoView(Command command) async {
    ScrollIntoView scrollIntoViewCommand = command;
    Finder target = await _waitForElement(_createFinder(scrollIntoViewCommand.finder));
223 224 225 226
    await Scrollable.ensureVisible(target.evaluate().single);
    return new ScrollResult();
  }

227 228 229
  Future<SetInputTextResult> _setInputText(Command command) async {
    SetInputText setInputTextCommand = command;
    Finder target = await _waitForElement(_createFinder(setInputTextCommand.finder));
230
    Input input = target.evaluate().single.widget;
231
    input.onChanged(new InputValue(text: setInputTextCommand.text));
232 233 234
    return new SetInputTextResult();
  }

235 236 237
  Future<SubmitInputTextResult> _submitInputText(Command command) async {
    SubmitInputText submitInputTextCommand = command;
    Finder target = await _waitForElement(_createFinder(submitInputTextCommand.finder));
238 239 240 241 242
    Input input = target.evaluate().single.widget;
    input.onSubmitted(input.value);
    return new SubmitInputTextResult(input.value.text);
  }

243 244 245
  Future<GetTextResult> _getText(Command command) async {
    GetText getTextCommand = command;
    Finder target = await _waitForElement(_createFinder(getTextCommand.finder));
246
    // TODO(yjbanov): support more ways to read text
247
    Text text = target.evaluate().single.widget;
248 249 250
    return new GetTextResult(text.data);
  }
}