web_driver.dart 11.1 KB
Newer Older
1 2 3 4 5 6
// 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';
7
import 'package:file/file.dart';
8 9
import 'package:matcher/matcher.dart';
import 'package:meta/meta.dart';
10 11

import 'package:path/path.dart' as path;
12
import 'package:vm_service/vm_service.dart' as vms;
13
import 'package:webdriver/async_io.dart' as async_io;
14 15 16 17
import 'package:webdriver/support/async.dart';

import '../common/error.dart';
import '../common/message.dart';
18 19

import 'common.dart';
20 21 22 23 24 25
import 'driver.dart';
import 'timeline.dart';

/// An implementation of the Flutter Driver using the WebDriver.
///
/// Example of how to test WebFlutterDriver:
26 27
///   1. Launch WebDriver binary: ./chromedriver --port=4444
///   2. Run test script: flutter drive --target=test_driver/scroll_perf_web.dart -d web-server --release
28 29
class WebFlutterDriver extends FlutterDriver {
  /// Creates a driver that uses a connection provided by the given
30
  /// [_connection].
31 32 33 34 35 36 37
  WebFlutterDriver.connectedTo(
    this._connection, {
    bool printCommunication = false,
    bool logCommunicationToFile = true,
  })  : _printCommunication = printCommunication,
        _logCommunicationToFile = logCommunicationToFile,
        _startTime = DateTime.now(),
38 39 40 41 42
        _driverId = _nextDriverId++
    {
      _logFilePathName = path.join(testOutputsDirectory, 'flutter_driver_commands_$_driverId.log');
    }

43 44 45

  final FlutterWebConnection _connection;
  DateTime _startTime;
46 47 48 49
  static int _nextDriverId = 0;

  /// The unique ID of this driver instance.
  final int _driverId;
50

51
  /// Start time for tracing.
52 53 54 55
  @visibleForTesting
  DateTime get startTime => _startTime;

  @override
56
  vms.Isolate get appIsolate => throw UnsupportedError('WebFlutterDriver does not support appIsolate');
57 58

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

61 62 63
  @override
  async_io.WebDriver get webDriver => _connection._driver;

64 65 66 67 68 69
  /// Whether to print communication between host and app to `stdout`.
  final bool _printCommunication;

  /// Whether to log communication between host and app to `flutter_driver_commands.log`.
  final bool _logCommunicationToFile;

70 71 72 73 74 75
  /// Logs are written here when _logCommunicationToFile is true.
  late final String _logFilePathName;

  /// Getter for file pathname where logs are written when _logCommunicationToFile is true
  String get logFilePathName => _logFilePathName;

76 77
  /// Creates a driver that uses a connection provided by the given
  /// [hostUrl] which would fallback to environment variable VM_SERVICE_URL.
78
  /// Driver also depends on environment variables DRIVER_SESSION_ID,
79 80 81
  /// BROWSER_SUPPORTS_TIMELINE, DRIVER_SESSION_URI, DRIVER_SESSION_SPEC,
  /// DRIVER_SESSION_CAPABILITIES and ANDROID_CHROME_ON_EMULATOR for
  /// configurations.
82 83 84 85 86 87 88 89
  ///
  /// See [FlutterDriver.connect] for more documentation.
  static Future<FlutterDriver> connectWeb({
    String? hostUrl,
    bool printCommunication = false,
    bool logCommunicationToFile = true,
    Duration? timeout,
  }) async {
90 91
    hostUrl ??= Platform.environment['VM_SERVICE_URL'];
    final Map<String, dynamic> settings = <String, dynamic>{
92 93 94 95
      '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'],
96
      'android-chrome-on-emulator': Platform.environment['ANDROID_CHROME_ON_EMULATOR'] == 'true',
97
      'session-capabilities': Platform.environment['DRIVER_SESSION_CAPABILITIES'],
98 99
    };
    final FlutterWebConnection connection = await FlutterWebConnection.connect
100
      (hostUrl!, settings, timeout: timeout);
101 102 103 104 105
    return WebFlutterDriver.connectedTo(
      connection,
      printCommunication: printCommunication,
      logCommunicationToFile: logCommunicationToFile,
    );
106 107 108 109 110 111
  }

  @override
  Future<Map<String, dynamic>> sendCommand(Command command) async {
    Map<String, dynamic> response;
    final Map<String, String> serialized = command.serialize();
112
    _logCommunication('>>> $serialized');
113
    try {
114
      final dynamic data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout);
115
      response = data != null ? (json.decode(data as String) as Map<String, dynamic>?)! : <String, dynamic>{};
116
      _logCommunication('<<< $response');
117
    } catch (error, stackTrace) {
118
      throw DriverError("Failed to respond to $command due to remote error\n : \$flutterDriver('${jsonEncode(serialized)}')",
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
          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();
  }

136 137 138 139 140
  void _logCommunication(String message) {
    if (_printCommunication) {
      driverLog('WebFlutterDriver', message);
    }
    if (_logCommunicationToFile) {
141 142
      assert(_logFilePathName != null);
      final File file = fs.file(_logFilePathName);
143 144 145 146 147
      file.createSync(recursive: true); // no-op if file exists
      file.writeAsStringSync('${DateTime.now()} $message\n', mode: FileMode.append, flush: true);
    }
  }

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
  @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>>[];
168
    for (final async_io.LogEntry entry in await _connection.logs.toList()) {
169
      if (_startTime.isBefore(entry.timestamp)) {
170
        final Map<String, dynamic> data = (jsonDecode(entry.message!) as Map<String, dynamic>)['message'] as Map<String, dynamic>;
171 172 173
        if (data['method'] == 'Tracing.dataCollected') {
          // 'ts' data collected from Chrome is in double format, conversion needed
          try {
174 175
            final Map<String, dynamic> params = data['params'] as Map<String, dynamic>;
            params['ts'] = double.parse(params['ts'].toString()).toInt();
176 177 178 179
          } on FormatException catch (_) {
            // data is corrupted, skip
            continue;
          }
180
          events.add(data['params']! as Map<String, dynamic>);
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
        }
      }
    }
    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();
  }

213
  /// Checks whether browser supports Timeline related operations.
214
  void _checkBrowserSupportsTimeline() {
215
    if (!_connection.supportsTimelineAction) {
216
      throw UnsupportedError('Timeline action is not supported by current testing browser');
217 218 219 220 221 222
    }
  }
}

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

227
  final async_io.WebDriver _driver;
228

229
  /// Whether the connected WebDriver supports timeline action for Flutter Web Driver.
230
  bool supportsTimelineAction;
231

232
  /// Starts WebDriver with the given [settings] and
233 234 235 236
  /// establishes the connection to Flutter Web application.
  static Future<FlutterWebConnection> connect(
      String url,
      Map<String, dynamic> settings,
237
      {Duration? timeout}) async {
238 239 240 241 242 243 244 245
    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()));
246 247 248 249 250 251 252
    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();
    }
253
    await driver.get(url);
254

255
    await waitUntilExtensionInstalled(driver, timeout);
256
    return FlutterWebConnection(driver, settings['support-timeline-action'] as bool);
257 258
  }

259
  /// Sends command via WebDriver to Flutter web application.
260
  Future<dynamic> sendCommand(String script, Duration? duration) async {
261 262
    dynamic result;
    try {
263
      await _driver.execute(script, <void>[]);
264 265 266 267 268
    } catch (_) {
      // In case there is an exception, do nothing
    }

    try {
269 270 271 272 273
      result = await waitFor<dynamic>(
        () => _driver.execute(r'return $flutterDriverResult', <String>[]),
        matcher: isNotNull,
        timeout: duration ?? const Duration(days: 30),
      );
274 275 276 277 278
    } catch (_) {
      // Returns null if exception thrown.
      return null;
    } finally {
      // Resets the result.
279
      await _driver.execute(r'''
280
        $flutterDriverResult = null
281 282 283 284 285 286
      ''', <void>[]);
    }
    return result;
  }

  /// Gets performance log from WebDriver.
287
  Stream<async_io.LogEntry> get logs => _driver.logs.get(async_io.LogType.performance);
288 289

  /// Takes screenshot via WebDriver.
290
  Future<List<int>> screenshot()  => _driver.captureScreenshotAsList();
291 292 293

  /// Closes the WebDriver.
  Future<void> close() async {
294
    await _driver.quit(closeSession: false);
295 296 297 298
  }
}

/// Waits until extension is installed.
299
Future<void> waitUntilExtensionInstalled(async_io.WebDriver driver, Duration? timeout) async {
300
  await waitFor<void>(() =>
301
      driver.execute(r'return typeof(window.$flutterDriver)', <String>[]),
302 303 304
      matcher: 'function',
      timeout: timeout ?? const Duration(days: 365));
}
305

306
async_io.WebDriverSpec _convertToSpec(String specString) {
307 308
  switch (specString.toLowerCase()) {
    case 'webdriverspec.w3c':
309
      return async_io.WebDriverSpec.W3c;
310
    case 'webdriverspec.jsonwire':
311
      return async_io.WebDriverSpec.JsonWire;
312
    default:
313
      return async_io.WebDriverSpec.Auto;
314 315
  }
}