// 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:async'; import 'package:file/src/interface/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/drive/web_driver_service.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/web/web_runner.dart'; import 'package:test/fake.dart'; import 'package:unified_analytics/unified_analytics.dart'; import 'package:webdriver/sync_io.dart' as sync_io; import '../../src/common.dart'; import '../../src/context.dart'; const List<String> kChromeArgs = <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', ]; void main() { testWithoutContext('getDesiredCapabilities Chrome with headless on', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'chrome', 'goog:loggingPrefs': <String, String>{ sync_io.LogType.browser: 'INFO', sync_io.LogType.performance: 'ALL', }, 'goog:chromeOptions': <String, dynamic>{ 'w3c': true, 'args': <String>[ ...kChromeArgs, '--headless', ], 'perfLoggingPrefs': <String, String>{ 'traceCategories': 'devtools.timeline,' 'v8,blink.console,benchmark,blink,' 'blink.user_timing', }, }, }; expect(getDesiredCapabilities(Browser.chrome, true), expected); }); testWithoutContext('getDesiredCapabilities Chrome with headless off', () { const String chromeBinary = 'random-binary'; final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'chrome', 'goog:loggingPrefs': <String, String>{ sync_io.LogType.browser: 'INFO', sync_io.LogType.performance: 'ALL', }, 'goog:chromeOptions': <String, dynamic>{ 'binary': chromeBinary, 'w3c': true, 'args': kChromeArgs, 'perfLoggingPrefs': <String, String>{ 'traceCategories': 'devtools.timeline,' 'v8,blink.console,benchmark,blink,' 'blink.user_timing', }, }, }; expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary: chromeBinary), expected); }); testWithoutContext('getDesiredCapabilities Chrome with browser flags', () { const List<String> webBrowserFlags = <String>[ '--autoplay-policy=no-user-gesture-required', '--incognito', '--auto-select-desktop-capture-source="Entire screen"', ]; final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'chrome', 'goog:loggingPrefs': <String, String>{ sync_io.LogType.browser: 'INFO', sync_io.LogType.performance: 'ALL', }, 'goog:chromeOptions': <String, dynamic>{ 'w3c': true, 'args': <String>[ ...kChromeArgs, '--autoplay-policy=no-user-gesture-required', '--incognito', '--auto-select-desktop-capture-source="Entire screen"', ], 'perfLoggingPrefs': <String, String>{ 'traceCategories': 'devtools.timeline,' 'v8,blink.console,benchmark,blink,' 'blink.user_timing', }, }, }; expect(getDesiredCapabilities(Browser.chrome, false, webBrowserFlags: webBrowserFlags), expected); }); testWithoutContext('getDesiredCapabilities Firefox with headless on', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'firefox', 'moz:firefoxOptions' : <String, dynamic>{ 'args': <String>['-headless'], '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, 'test.currentTimeOffsetSeconds': 11491200, }, 'log': <String, String>{'level': 'trace'}, }, }; expect(getDesiredCapabilities(Browser.firefox, true), expected); }); testWithoutContext('getDesiredCapabilities Firefox with headless off', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'firefox', 'moz:firefoxOptions' : <String, dynamic>{ 'args': <String>[], '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, 'test.currentTimeOffsetSeconds': 11491200, }, 'log': <String, String>{'level': 'trace'}, }, }; expect(getDesiredCapabilities(Browser.firefox, false), expected); }); testWithoutContext('getDesiredCapabilities Firefox with browser flags', () { const List<String> webBrowserFlags = <String>[ '-url=https://example.com', '-private', ]; final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'firefox', 'moz:firefoxOptions' : <String, dynamic>{ 'args': <String>[ '-url=https://example.com', '-private', ], '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, 'test.currentTimeOffsetSeconds': 11491200, }, 'log': <String, String>{'level': 'trace'}, }, }; expect(getDesiredCapabilities(Browser.firefox, false, webBrowserFlags: webBrowserFlags), expected); }); testWithoutContext('getDesiredCapabilities Edge', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'edge', }; expect(getDesiredCapabilities(Browser.edge, false), expected); }); testWithoutContext('getDesiredCapabilities macOS Safari', () { final Map<String, dynamic> expected = <String, dynamic>{ 'browserName': 'safari', }; expect(getDesiredCapabilities(Browser.safari, false), expected); }); testWithoutContext('getDesiredCapabilities iOS Safari', () { final Map<String, dynamic> expected = <String, dynamic>{ 'platformName': 'ios', 'browserName': 'safari', 'safari:useSimulator': true, }; expect(getDesiredCapabilities(Browser.iosSafari, false), expected); }); testWithoutContext('getDesiredCapabilities android chrome', () { const List<String> webBrowserFlags = <String>[ '--autoplay-policy=no-user-gesture-required', '--incognito', ]; final Map<String, dynamic> expected = <String, dynamic>{ 'browserName': 'chrome', 'platformName': 'android', 'goog:chromeOptions': <String, dynamic>{ 'androidPackage': 'com.android.chrome', 'args': <String>[ '--disable-fullscreen', '--autoplay-policy=no-user-gesture-required', '--incognito', ], }, }; expect(getDesiredCapabilities(Browser.androidChrome, false, webBrowserFlags: webBrowserFlags), expected); }); testUsingContext('WebDriverService starts and stops an app', () async { final WebDriverService service = setUpDriverService(); final FakeDevice device = FakeDevice(); await service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true); await service.stop(); expect(FakeResidentRunner.instance.callLog, <String>[ 'run', 'exitApp', 'cleanupAtFinish', ]); }, overrides: <Type, Generator>{ WebRunnerFactory: () => FakeWebRunnerFactory(), }); testUsingContext('WebDriverService can start an app with a launch url provided', () async { final WebDriverService service = setUpDriverService(); final FakeDevice device = FakeDevice(); const String testUrl = 'http://localhost:1234/test'; await service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, webLaunchUrl: testUrl), true); await service.stop(); expect(service.webUri, Uri.parse(testUrl)); }, overrides: <Type, Generator>{ WebRunnerFactory: () => FakeWebRunnerFactory(), }); testUsingContext('WebDriverService will throw when an invalid launch url is provided', () async { final WebDriverService service = setUpDriverService(); final FakeDevice device = FakeDevice(); const String invalidTestUrl = '::INVALID_URL::'; await expectLater( service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile, webLaunchUrl: invalidTestUrl), true), throwsA(isA<FormatException>()), ); }, overrides: <Type, Generator>{ WebRunnerFactory: () => FakeWebRunnerFactory(), }); testUsingContext('WebDriverService forwards exception when run future fails before app starts', () async { final WebDriverService service = setUpDriverService(); final Device device = FakeDevice(); await expectLater( service.start(BuildInfo.profile, device, DebuggingOptions.enabled(BuildInfo.profile), true), throwsA('This is a test error'), ); }, overrides: <Type, Generator>{ WebRunnerFactory: () => FakeWebRunnerFactory( doResolveToError: true, ), }); } class FakeWebRunnerFactory implements WebRunnerFactory { FakeWebRunnerFactory({ this.doResolveToError = false, }); final bool doResolveToError; @override ResidentRunner createWebRunner( FlutterDevice device, { String? target, bool? stayResident, FlutterProject? flutterProject, bool? ipv6, DebuggingOptions? debuggingOptions, UrlTunneller? urlTunneller, Logger? logger, FileSystem? fileSystem, SystemClock? systemClock, Usage? usage, Analytics? analytics, bool machine = false, }) { expect(stayResident, isTrue); return FakeResidentRunner( doResolveToError: doResolveToError, ); } } class FakeResidentRunner extends Fake implements ResidentRunner { FakeResidentRunner({ required this.doResolveToError, }) { instance = this; } static late FakeResidentRunner instance; final bool doResolveToError; final Completer<int> _exitCompleter = Completer<int>(); final List<String> callLog = <String>[]; @override Uri get uri => Uri(); @override Future<int> run({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool enableDevTools = false, String? route, }) async { callLog.add('run'); if (doResolveToError) { return Future<int>.error('This is a test error'); } appStartedCompleter?.complete(); // Emulate stayResident by completing after exitApp is called. return _exitCompleter.future; } @override Future<void> exitApp() async { callLog.add('exitApp'); _exitCompleter.complete(0); } @override Future<void> cleanupAtFinish() async { callLog.add('cleanupAtFinish'); } } WebDriverService setUpDriverService() { final BufferLogger logger = BufferLogger.test(); return WebDriverService( logger: logger, processUtils: ProcessUtils( logger: logger, processManager: FakeProcessManager.any(), ), dartSdkPath: 'dart', ); } class FakeDevice extends Fake implements Device { @override final PlatformType platformType = PlatformType.web; @override Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm; }