web_driver_service.dart 10.9 KB
Newer Older
1 2 3 4
// 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.

5

6

7 8 9 10 11
import 'dart:async';
import 'dart:math' as math;

import 'package:file/file.dart';
import 'package:meta/meta.dart';
12
import 'package:package_config/package_config.dart';
13 14 15
import 'package:webdriver/async_io.dart' as async_io;

import '../base/common.dart';
16
import '../base/io.dart';
17
import '../base/logger.dart';
18 19 20 21
import '../base/process.dart';
import '../build_info.dart';
import '../convert.dart';
import '../device.dart';
22
import '../globals.dart' as globals;
23 24 25 26 27 28 29 30
import '../project.dart';
import '../resident_runner.dart';
import '../web/web_runner.dart';
import 'drive_service.dart';

/// An implementation of the driver service for web debug and release applications.
class WebDriverService extends DriverService {
  WebDriverService({
31 32 33
    required ProcessUtils processUtils,
    required String dartSdkPath,
    required Logger logger,
34
  }) : _processUtils = processUtils,
35 36
       _dartSdkPath = dartSdkPath,
       _logger = logger;
37 38 39

  final ProcessUtils _processUtils;
  final String _dartSdkPath;
40
  final Logger _logger;
41

42 43
  late ResidentRunner _residentRunner;
  Uri? _webUri;
44

45 46 47 48 49
  /// The result of [ResidentRunner.run].
  ///
  /// This is expected to stay `null` throughout the test, as the application
  /// must be running until [stop] is called. If it becomes non-null, it likely
  /// indicates a bug.
50
  int? _runResult;
51

52 53 54 55 56 57
  @override
  Future<void> start(
    BuildInfo buildInfo,
    Device device,
    DebuggingOptions debuggingOptions,
    bool ipv6, {
58 59 60 61
    File? applicationBinary,
    String? route,
    String? userIdentifier,
    String? mainPath,
62 63 64 65 66 67 68 69
    Map<String, Object> platformArgs = const <String, Object>{},
  }) async {
    final FlutterDevice flutterDevice = await FlutterDevice.create(
      device,
      target: mainPath,
      buildInfo: buildInfo,
      platform: globals.platform,
    );
70
    _residentRunner = webRunnerFactory!.createWebRunner(
71 72 73 74 75 76 77 78 79 80 81 82 83
      flutterDevice,
      target: mainPath,
      ipv6: ipv6,
      debuggingOptions: buildInfo.isRelease ?
        DebuggingOptions.disabled(
          buildInfo,
          port: debuggingOptions.port,
        )
        : DebuggingOptions.enabled(
          buildInfo,
          port: debuggingOptions.port,
          disablePortPublication: debuggingOptions.disablePortPublication,
        ),
84
      stayResident: true,
85 86
      urlTunneller: null,
      flutterProject: FlutterProject.current(),
87 88
      fileSystem: globals.fs,
      usage: globals.flutterUsage,
89
      logger: _logger,
90
      systemClock: globals.systemClock,
91 92
    );
    final Completer<void> appStartedCompleter = Completer<void>.sync();
93
    final Future<int?> runFuture = _residentRunner.run(
94 95 96
      appStartedCompleter: appStartedCompleter,
      route: route,
    );
97 98 99

    bool isAppStarted = false;
    await Future.any<Object>(<Future<Object>>[
100
      runFuture.then((int? result) {
101 102
        _runResult = result;
        return null;
103
      } as FutureOr<Object> Function(int?)),
104 105 106
      appStartedCompleter.future.then((_) {
        isAppStarted = true;
        return null;
107
      } as FutureOr<Object> Function(void)),
108 109 110 111 112 113 114 115 116 117 118 119 120
    ]);

    if (_runResult != null) {
      throw ToolExit(
        'Application exited before the test started. Check web driver logs '
        'for possible application-side errors.'
      );
    }

    if (!isAppStarted) {
      throw ToolExit('Failed to start application');
    }

121
    _webUri = _residentRunner.uri;
122 123 124

    if (_webUri == null) {
      throw ToolExit('Unable to connect to the app. URL not available.');
125 126 127 128
    }
  }

  @override
129 130 131 132 133 134 135 136 137 138
  Future<int> startTest(
    String testFile,
    List<String> arguments,
    Map<String, String> environment,
    PackageConfig packageConfig, {
    bool? headless,
    String? chromeBinary,
    String? browserName,
    bool? androidEmulator,
    int? driverPort,
139
    List<String> webBrowserFlags = const <String>[],
140 141
    List<String>? browserDimension,
    String? profileMemory,
142
  }) async {
143
    late async_io.WebDriver webDriver;
144 145 146 147
    final Browser browser = _browserNameToEnum(browserName);
    try {
      webDriver = await async_io.createDriver(
        uri: Uri.parse('http://localhost:$driverPort/'),
148 149 150 151 152 153
        desired: getDesiredCapabilities(
          browser,
          headless,
          webBrowserFlags: webBrowserFlags,
          chromeBinary: chromeBinary,
        ),
154
      );
155 156
    } on SocketException catch (error) {
      _logger.printTrace('$error');
157
      throwToolExit(
158 159 160
        'Unable to start a WebDriver session for web testing.\n'
        'Make sure you have the correct WebDriver server (e.g. chromedriver) running at $driverPort.\n'
        'For instructions on how to obtain and run a WebDriver server, see:\n'
161
        'https://flutter.dev/docs/testing/integration-tests#running-in-a-browser\n'
162 163 164 165 166 167
      );
    }

    final bool isAndroidChrome = browser == Browser.androidChrome;
    // Do not set the window size for android chrome browser.
    if (!isAndroidChrome) {
168 169 170
      assert(browserDimension!.length == 2);
      late int x;
      late int y;
171
      try {
172
        x = int.parse(browserDimension![0]);
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
        y = int.parse(browserDimension[1]);
      } on FormatException catch (ex) {
        throwToolExit('Dimension provided to --browser-dimension is invalid: $ex');
      }
      final async_io.Window window = await webDriver.window;
      await window.setLocation(const math.Point<int>(0, 0));
      await window.setSize(math.Rectangle<int>(0, 0, x, y));
    }
    final int result = await _processUtils.stream(<String>[
      _dartSdkPath,
      ...arguments,
      testFile,
      '-rexpanded',
    ], environment: <String, String>{
      'VM_SERVICE_URL': _webUri.toString(),
      ..._additionalDriverEnvironment(webDriver, browserName, androidEmulator),
      ...environment,
    });
    await webDriver.quit();
    return result;
  }

  @override
196
  Future<void> stop({File? writeSkslOnExit, String? userIdentifier}) async {
197 198
    final bool appDidFinishPrematurely = _runResult != null;
    await _residentRunner.exitApp();
199
    await _residentRunner.cleanupAtFinish();
200 201 202 203 204 205 206

    if (appDidFinishPrematurely) {
      throw ToolExit(
        'Application exited before the test finished. Check web driver logs '
        'for possible application-side errors.'
      );
    }
207 208
  }

209
  Map<String, String> _additionalDriverEnvironment(async_io.WebDriver webDriver, String? browserName, bool? androidEmulator) {
210 211 212 213 214 215 216
    return <String, String>{
      'DRIVER_SESSION_ID': webDriver.id,
      'DRIVER_SESSION_URI': webDriver.uri.toString(),
      'DRIVER_SESSION_SPEC': webDriver.spec.toString(),
      'DRIVER_SESSION_CAPABILITIES': json.encode(webDriver.capabilities),
      'SUPPORT_TIMELINE_ACTION': (_browserNameToEnum(browserName) == Browser.chrome).toString(),
      'FLUTTER_WEB_TEST': 'true',
217
      'ANDROID_CHROME_ON_EMULATOR': (_browserNameToEnum(browserName) == Browser.androidChrome && androidEmulator!).toString(),
218 219
    };
  }
220 221 222 223 224

  @override
  Future<void> reuseApplication(Uri vmServiceUri, Device device, DebuggingOptions debuggingOptions, bool ipv6) async {
    throwToolExit('--use-existing-app is not supported with flutter web driver');
  }
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
}

/// A list of supported browsers.
enum Browser {
  /// Chrome on Android: https://developer.chrome.com/multidevice/android/overview
  androidChrome,
  /// Chrome: https://www.google.com/chrome/
  chrome,
  /// Edge: https://www.microsoft.com/en-us/windows/microsoft-edge
  edge,
  /// Firefox: https://www.mozilla.org/en-US/firefox/
  firefox,
  /// Safari in iOS: https://www.apple.com/safari/
  iosSafari,
  /// Safari in macOS: https://www.apple.com/safari/
  safari,
}

243 244
/// Returns desired capabilities for given [browser], [headless], [chromeBinary]
/// and [webBrowserFlags].
245
@visibleForTesting
246 247 248 249 250 251
Map<String, dynamic> getDesiredCapabilities(
  Browser browser,
  bool? headless, {
  List<String> webBrowserFlags = const <String>[],
  String? chromeBinary,
}) {
252 253 254 255 256
  switch (browser) {
    case Browser.chrome:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'chrome',
257 258 259 260
        'goog:loggingPrefs': <String, String>{
          async_io.LogType.browser: 'INFO',
          async_io.LogType.performance: 'ALL',
        },
261 262 263 264 265 266 267 268 269 270 271 272 273 274
        'chromeOptions': <String, dynamic>{
          if (chromeBinary != null)
            'binary': chromeBinary,
          'w3c': false,
          'args': <String>[
            '--bwsi',
            '--disable-background-timer-throttling',
            '--disable-default-apps',
            '--disable-extensions',
            '--disable-popup-blocking',
            '--disable-translate',
            '--no-default-browser-check',
            '--no-sandbox',
            '--no-first-run',
275
            if (headless!) '--headless',
276
            ...webBrowserFlags,
277 278 279 280
          ],
          'perfLoggingPrefs': <String, String>{
            'traceCategories':
            'devtools.timeline,'
281 282 283
            'v8,blink.console,benchmark,blink,'
            'blink.user_timing',
          },
284 285 286 287 288 289 290 291
        },
      };
    case Browser.firefox:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'firefox',
        'moz:firefoxOptions' : <String, dynamic>{
          'args': <String>[
292
            if (headless!) '-headless',
293
            ...webBrowserFlags,
294 295 296 297 298 299 300 301 302
          ],
          'prefs': <String, dynamic>{
            'dom.file.createInChild': true,
            'dom.timeout.background_throttling_max_budget': -1,
            'media.autoplay.default': 0,
            'media.gmp-manager.url': '',
            'media.gmp-provider.enabled': false,
            'network.captive-portal-service.enabled': false,
            'security.insecure_field_warning.contextual.enabled': false,
303
            'test.currentTimeOffsetSeconds': 11491200,
304
          },
305 306
          'log': <String, String>{'level': 'trace'},
        },
307 308 309 310 311 312 313 314 315 316 317 318 319 320
      };
    case Browser.edge:
      return <String, dynamic>{
        'acceptInsecureCerts': true,
        'browserName': 'edge',
      };
    case Browser.safari:
      return <String, dynamic>{
        'browserName': 'safari',
      };
    case Browser.iosSafari:
      return <String, dynamic>{
        'platformName': 'ios',
        'browserName': 'safari',
321
        'safari:useSimulator': true,
322 323 324 325 326 327 328
      };
    case Browser.androidChrome:
      return <String, dynamic>{
        'browserName': 'chrome',
        'platformName': 'android',
        'goog:chromeOptions': <String, dynamic>{
          'androidPackage': 'com.android.chrome',
329 330 331 332
          'args': <String>[
            '--disable-fullscreen',
            ...webBrowserFlags,
          ],
333 334 335 336 337 338
        },
      };
  }
}

/// Converts [browserName] string to [Browser]
339
Browser _browserNameToEnum(String? browserName) {
340 341 342 343 344 345 346 347
  switch (browserName) {
    case 'android-chrome': return Browser.androidChrome;
    case 'chrome': return Browser.chrome;
    case 'edge': return Browser.edge;
    case 'firefox': return Browser.firefox;
    case 'ios-safari': return Browser.iosSafari;
    case 'safari': return Browser.safari;
  }
348
  throw UnsupportedError('Browser $browserName not supported');
349
}