web_driver.dart 10.1 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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';
10
import 'package:vm_service/vm_service.dart' as vms;
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
  @visibleForTesting
  DateTime get startTime => _startTime;

  @override
39
  vms.Isolate get appIsolate => throw UnsupportedError('WebFlutterDriver does not support appIsolate');
40 41

  @override
42
  vms.VmService get serviceClient => throw UnsupportedError('WebFlutterDriver does not support serviceClient');
43

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
  static Future<FlutterDriver> connectWeb(
54
      {String? hostUrl, Duration? timeout}) async {
55 56
    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
    };
    final FlutterWebConnection connection = await FlutterWebConnection.connect
65
      (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
      response = data != null ? (json.decode(data as String) as Map<String, dynamic>?)! : <String, dynamic>{};
92
    } 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
      if (_startTime.isBefore(entry.timestamp)) {
133
        final Map<String, dynamic> data = jsonDecode(entry.message!)['message'] as Map<String, dynamic>;
134 135 136 137 138 139 140 141 142
        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;
          }
143
          events.add(data['params']! as Map<String, dynamic>);
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
        }
      }
    }
    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 189 190 191 192
  FlutterWebConnection(this._driver, this.supportsTimelineAction) {
    _driver.logs.get(async_io.LogType.browser).listen((async_io.LogEntry entry) {
      print('[${entry.level}]: ${entry.message}');
    });
  }
193

194
  final async_io.WebDriver _driver;
195

196
  /// Whether the connected WebDriver supports timeline action for Flutter Web Driver.
197
  bool supportsTimelineAction;
198

199
  /// Starts WebDriver with the given [settings] and
200 201 202 203
  /// establishes the connection to Flutter Web application.
  static Future<FlutterWebConnection> connect(
      String url,
      Map<String, dynamic> settings,
204
      {Duration? timeout}) async {
205 206 207 208 209 210 211 212
    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()));
213 214 215 216 217 218 219
    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();
    }
220
    await driver.get(url);
221

222
    await waitUntilExtensionInstalled(driver, timeout);
223
    return FlutterWebConnection(driver, settings['support-timeline-action'] as bool);
224 225
  }

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

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

  /// Gets performance log from WebDriver.
254
  Stream<async_io.LogEntry> get logs => _driver.logs.get(async_io.LogType.performance);
255 256

  /// Takes screenshot via WebDriver.
257
  Future<List<int>> screenshot()  => _driver.captureScreenshotAsList();
258 259 260

  /// Closes the WebDriver.
  Future<void> close() async {
261
    await _driver.quit(closeSession: false);
262 263 264 265
  }
}

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

273
async_io.WebDriverSpec _convertToSpec(String specString) {
274 275
  switch (specString.toLowerCase()) {
    case 'webdriverspec.w3c':
276
      return async_io.WebDriverSpec.W3c;
277
    case 'webdriverspec.jsonwire':
278
      return async_io.WebDriverSpec.JsonWire;
279
    default:
280
      return async_io.WebDriverSpec.Auto;
281 282
  }
}