web_driver.dart 9.95 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
// Copyright 2014 The Flutter 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:convert';
import 'dart:io';

import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
import 'package:vm_service_client/vm_service_client.dart';
11
import 'package:webdriver/async_io.dart' as async_io;
12 13 14 15 16 17 18 19 20 21
import 'package:webdriver/support/async.dart';

import '../common/error.dart';
import '../common/message.dart';
import 'driver.dart';
import 'timeline.dart';

/// An implementation of the Flutter Driver using the WebDriver.
///
/// Example of how to test WebFlutterDriver:
22 23
///   1. Launch WebDriver binary: ./chromedriver --port=4444
///   2. Run test script: flutter drive --target=test_driver/scroll_perf_web.dart -d web-server --release
24 25
class WebFlutterDriver extends FlutterDriver {
  /// Creates a driver that uses a connection provided by the given
26 27
  /// [_connection].
  WebFlutterDriver.connectedTo(this._connection) :
28 29 30 31
        _startTime = DateTime.now();

  final FlutterWebConnection _connection;
  DateTime _startTime;
32
  bool _accessibilityEnabled = false;
33

34
  /// Start time for tracing.
35 36 37 38 39 40 41 42 43
  @visibleForTesting
  DateTime get startTime => _startTime;

  @override
  VMIsolate get appIsolate => throw UnsupportedError('WebFlutterDriver does not support appIsolate');

  @override
  VMServiceClient get serviceClient => throw UnsupportedError('WebFlutterDriver does not support serviceClient');

44 45 46
  @override
  async_io.WebDriver get webDriver => _connection._driver;

47 48
  /// Creates a driver that uses a connection provided by the given
  /// [hostUrl] which would fallback to environment variable VM_SERVICE_URL.
49
  /// Driver also depends on environment variables DRIVER_SESSION_ID,
50 51 52
  /// BROWSER_SUPPORTS_TIMELINE, DRIVER_SESSION_URI, DRIVER_SESSION_SPEC,
  /// DRIVER_SESSION_CAPABILITIES and ANDROID_CHROME_ON_EMULATOR for
  /// configurations.
53 54 55 56
  static Future<FlutterDriver> connectWeb(
      {String hostUrl, Duration timeout}) async {
    hostUrl ??= Platform.environment['VM_SERVICE_URL'];
    final Map<String, dynamic> settings = <String, dynamic>{
57 58 59 60
      'support-timeline-action': Platform.environment['SUPPORT_TIMELINE_ACTION'] == 'true',
      'session-id': Platform.environment['DRIVER_SESSION_ID'],
      'session-uri': Platform.environment['DRIVER_SESSION_URI'],
      'session-spec': Platform.environment['DRIVER_SESSION_SPEC'],
61
      'android-chrome-on-emulator': Platform.environment['ANDROID_CHROME_ON_EMULATOR'] == 'true',
62
      'session-capabilities': Platform.environment['DRIVER_SESSION_CAPABILITIES'],
63 64 65
    };
    final FlutterWebConnection connection = await FlutterWebConnection.connect
      (hostUrl, settings, timeout: timeout);
66
    return WebFlutterDriver.connectedTo(connection);
67 68
  }

69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  @override
  Future<void> enableAccessibility() async {
    if (!_accessibilityEnabled) {
      // Clicks the button to enable accessibility via Javascript for Desktop Web.
      //
      // The tag used in the script is based on
      // https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart#L193
      //
      // TODO(angjieli): Support Mobile Web. (https://github.com/flutter/flutter/issues/65192)
      await webDriver.execute(
          'document.querySelector(\'flt-semantics-placeholder\').click();',
          <String>[]);
      _accessibilityEnabled = true;
    }
  }

85 86 87 88 89
  @override
  Future<Map<String, dynamic>> sendCommand(Command command) async {
    Map<String, dynamic> response;
    final Map<String, String> serialized = command.serialize();
    try {
90
      final dynamic data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout);
91 92
      response = data != null ? json.decode(data as String) as Map<String, dynamic> : <String, dynamic>{};
    } catch (error, stackTrace) {
93
      throw DriverError("Failed to respond to $command due to remote error\n : \$flutterDriver('${jsonEncode(serialized)}')",
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
          error,
          stackTrace
      );
    }
    if (response['isError'] == true)
      throw DriverError('Error in Flutter application: ${response['response']}');
    return response['response'] as Map<String, dynamic>;
  }

  @override
  Future<void> close() => _connection.close();

  @override
  Future<void> waitUntilFirstFrameRasterized() async {
    throw UnimplementedError();
  }

  @override
  Future<List<int>> screenshot() async {
    await Future<void>.delayed(const Duration(seconds: 2));

    return _connection.screenshot();
  }

  @override
  Future<void> startTracing({
    List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
    Duration timeout = kUnusuallyLongTimeout,
  }) async {
    _checkBrowserSupportsTimeline();
  }

  @override
  Future<Timeline> stopTracingAndDownloadTimeline({Duration timeout = kUnusuallyLongTimeout}) async {
    _checkBrowserSupportsTimeline();

    final List<Map<String, dynamic>> events = <Map<String, dynamic>>[];
131
    for (final async_io.LogEntry entry in await _connection.logs.toList()) {
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
      if (_startTime.isBefore(entry.timestamp)) {
        final Map<String, dynamic> data = jsonDecode(entry.message)['message'] as Map<String, dynamic>;
        if (data['method'] == 'Tracing.dataCollected') {
          // 'ts' data collected from Chrome is in double format, conversion needed
          try {
            data['params']['ts'] =
                double.parse(data['params']['ts'].toString()).toInt();
          } on FormatException catch (_) {
            // data is corrupted, skip
            continue;
          }
          events.add(data['params'] as Map<String, dynamic>);
        }
      }
    }
    final Map<String, dynamic> json = <String, dynamic>{
      'traceEvents': events,
    };
    return Timeline.fromJson(json);
  }

  @override
  Future<Timeline> traceAction(Future<dynamic> Function() action, {
    List<TimelineStream> streams = const <TimelineStream>[TimelineStream.all],
    bool retainPriorEvents = false,
  }) async {
    _checkBrowserSupportsTimeline();
    if (!retainPriorEvents) {
      await clearTimeline();
    }
    await startTracing(streams: streams);
    await action();

    return stopTracingAndDownloadTimeline();
  }

  @override
  Future<void> clearTimeline({Duration timeout = kUnusuallyLongTimeout}) async {
    _checkBrowserSupportsTimeline();

    // Reset start time
    _startTime = DateTime.now();
  }

176
  /// Checks whether browser supports Timeline related operations.
177
  void _checkBrowserSupportsTimeline() {
178
    if (!_connection.supportsTimelineAction) {
179
      throw UnsupportedError('Timeline action is not supported by current testing browser');
180 181 182 183 184 185
    }
  }
}

/// Encapsulates connection information to an instance of a Flutter Web application.
class FlutterWebConnection {
186
  /// Creates a FlutterWebConnection with WebDriver
187
  /// and whether the WebDriver supports timeline action.
188
  FlutterWebConnection(this._driver, this.supportsTimelineAction);
189

190
  final async_io.WebDriver _driver;
191

192
  /// Whether the connected WebDriver supports timeline action for Flutter Web Driver.
193
  bool supportsTimelineAction;
194

195
  /// Starts WebDriver with the given [settings] and
196 197 198 199 200
  /// establishes the connection to Flutter Web application.
  static Future<FlutterWebConnection> connect(
      String url,
      Map<String, dynamic> settings,
      {Duration timeout}) async {
201 202 203 204 205 206 207 208
    final String sessionId = settings['session-id'].toString();
    final Uri sessionUri = Uri.parse(settings['session-uri'].toString());
    final async_io.WebDriver driver = async_io.WebDriver(
        sessionUri,
        sessionId,
        json.decode(settings['session-capabilities'] as String) as Map<String, dynamic>,
        async_io.AsyncIoRequestClient(sessionUri.resolve('session/$sessionId/')),
        _convertToSpec(settings['session-spec'].toString().toLowerCase()));
209 210 211 212 213 214 215
    if (settings['android-chrome-on-emulator'] == true) {
      final Uri localUri = Uri.parse(url);
      // Converts to Android Emulator Uri.
      // Hardcode the host to 10.0.2.2 based on
      // https://developer.android.com/studio/run/emulator-networking
      url = Uri(scheme: localUri.scheme, host: '10.0.2.2', port:localUri.port).toString();
    }
216
    await driver.get(url);
217

218
    await waitUntilExtensionInstalled(driver, timeout);
219
    return FlutterWebConnection(driver, settings['support-timeline-action'] as bool);
220 221
  }

222
  /// Sends command via WebDriver to Flutter web application.
223 224 225
  Future<dynamic> sendCommand(String script, Duration duration) async {
    dynamic result;
    try {
226
      await _driver.execute(script, <void>[]);
227 228 229 230 231
    } catch (_) {
      // In case there is an exception, do nothing
    }

    try {
232 233 234 235 236
      result = await waitFor<dynamic>(
        () => _driver.execute(r'return $flutterDriverResult', <String>[]),
        matcher: isNotNull,
        timeout: duration ?? const Duration(days: 30),
      );
237 238 239 240 241
    } catch (_) {
      // Returns null if exception thrown.
      return null;
    } finally {
      // Resets the result.
242
      await _driver.execute(r'''
243
        $flutterDriverResult = null
244 245 246 247 248 249
      ''', <void>[]);
    }
    return result;
  }

  /// Gets performance log from WebDriver.
250
  Stream<async_io.LogEntry> get logs => _driver.logs.get(async_io.LogType.performance);
251 252

  /// Takes screenshot via WebDriver.
253
  Future<List<int>> screenshot()  => _driver.captureScreenshotAsList();
254 255 256

  /// Closes the WebDriver.
  Future<void> close() async {
257
    await _driver.quit(closeSession: false);
258 259 260 261
  }
}

/// Waits until extension is installed.
262
Future<void> waitUntilExtensionInstalled(async_io.WebDriver driver, Duration timeout) async {
263
  await waitFor<void>(() =>
264
      driver.execute(r'return typeof(window.$flutterDriver)', <String>[]),
265 266 267
      matcher: 'function',
      timeout: timeout ?? const Duration(days: 365));
}
268

269
async_io.WebDriverSpec _convertToSpec(String specString) {
270 271
  switch (specString.toLowerCase()) {
    case 'webdriverspec.w3c':
272
      return async_io.WebDriverSpec.W3c;
273
    case 'webdriverspec.jsonwire':
274
      return async_io.WebDriverSpec.JsonWire;
275
    default:
276
      return async_io.WebDriverSpec.Auto;
277 278
  }
}