// 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/memory.dart'; import 'package:platform/platform.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/drive.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:webdriver/sync_io.dart' as sync_io; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/mocks.dart'; void main() { group('drive', () { DriveCommand command; Device mockUnsupportedDevice; MemoryFileSystem fs; Directory tempDir; setUpAll(() { Cache.disableLocking(); }); setUp(() { command = DriveCommand(); applyMocksToCommand(command); fs = MemoryFileSystem(); tempDir = fs.systemTempDirectory.createTempSync('flutter_drive_test.'); fs.currentDirectory = tempDir; fs.directory('test').createSync(); fs.directory('test_driver').createSync(); fs.file('pubspec.yaml')..createSync(); fs.file('.packages').createSync(); setExitFunctionForTests(); appStarter = (DriveCommand command) { throw 'Unexpected call to appStarter'; }; testRunner = (List<String> testArgs, Map<String, String> environment) { throw 'Unexpected call to testRunner'; }; appStopper = (DriveCommand command) { throw 'Unexpected call to appStopper'; }; }); tearDown(() { command = null; restoreExitFunction(); restoreAppStarter(); restoreAppStopper(); restoreTestRunner(); tryToDelete(tempDir); }); testUsingContext('returns 1 when test file is not found', () async { testDeviceManager.addDevice(MockDevice()); final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); globals.fs.file(testApp).createSync(recursive: true); final List<String> args = <String>[ 'drive', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 1); expect(e.message, contains('Test file not found: $testFile')); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 1 when app fails to run', () async { testDeviceManager.addDevice(MockDevice()); appStarter = expectAsync1((DriveCommand command) async => null); final String testApp = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e.dart'); final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); final MemoryFileSystem memFs = fs; await memFs.file(testApp).writeAsString('main() { }'); await memFs.file(testFile).writeAsString('main() { }'); final List<String> args = <String>[ 'drive', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode, 1); expect(e.message, contains('Application failed to start. Will not run test. Quitting.')); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 1 when app file is outside package', () async { final String appFile = globals.fs.path.join(tempDir.dirname, 'other_app', 'app.dart'); globals.fs.file(appFile).createSync(recursive: true); final List<String> args = <String>[ '--no-wrap', 'drive', '--target=$appFile', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 1); expect(testLogger.errorText, contains( 'Application file $appFile is outside the package directory ${tempDir.path}', )); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 1 when app file is in the root dir', () async { final String appFile = globals.fs.path.join(tempDir.path, 'main.dart'); globals.fs.file(appFile).createSync(recursive: true); final List<String> args = <String>[ '--no-wrap', 'drive', '--target=$appFile', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 1); expect(testLogger.errorText, contains( 'Application file main.dart must reside in one of the ' 'sub-directories of the package structure, not in the root directory.', )); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 0 when test ends successfully', () async { testDeviceManager.addDevice(MockDevice()); final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); appStarter = expectAsync1((DriveCommand command) async { return LaunchResult.succeeded(); }); testRunner = expectAsync2((List<String> testArgs, Map<String, String> environment) async { expect(testArgs, <String>[testFile]); // VM_SERVICE_URL is not set by drive command arguments expect(environment, <String, String>{ 'VM_SERVICE_URL': 'null', }); return null; }); appStopper = expectAsync1((DriveCommand command) async { return true; }); final MemoryFileSystem memFs = fs; await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testFile).writeAsString('main() {}'); final List<String> args = <String>[ 'drive', '--target=$testApp', '--no-pub', ]; await createTestCommandRunner(command).run(args); expect(testLogger.errorText, isEmpty); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns exitCode set by test runner', () async { testDeviceManager.addDevice(MockDevice()); final String testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); final String testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); appStarter = expectAsync1((DriveCommand command) async { return LaunchResult.succeeded(); }); testRunner = (List<String> testArgs, Map<String, String> environment) async { throwToolExit(null, exitCode: 123); }; appStopper = expectAsync1((DriveCommand command) async { return true; }); final MemoryFileSystem memFs = fs; await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testFile).writeAsString('main() {}'); final List<String> args = <String>[ 'drive', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); fail('Expect exception'); } on ToolExit catch (e) { expect(e.exitCode ?? 1, 123); expect(e.message, isNull); } }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); group('findTargetDevice', () { testUsingContext('uses specified device', () async { testDeviceManager.specifiedDeviceId = '123'; final Device mockDevice = MockDevice(); testDeviceManager.addDevice(mockDevice); when(mockDevice.name).thenReturn('specified-device'); when(mockDevice.id).thenReturn('123'); final Device device = await findTargetDevice(); expect(device.name, 'specified-device'); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); }); void findTargetDeviceOnOperatingSystem(String operatingSystem) { Platform platform() => FakePlatform(operatingSystem: operatingSystem); testUsingContext('returns null if no devices found', () async { expect(await findTargetDevice(), isNull); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Platform: platform, }); testUsingContext('uses existing Android device', () async { final Device mockDevice = MockAndroidDevice(); when(mockDevice.name).thenReturn('mock-android-device'); testDeviceManager.addDevice(mockDevice); final Device device = await findTargetDevice(); expect(device.name, 'mock-android-device'); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Platform: platform, }); testUsingContext('skips unsupported device', () async { final Device mockDevice = MockAndroidDevice(); mockUnsupportedDevice = MockDevice(); when(mockUnsupportedDevice.isSupportedForProject(any)) .thenReturn(false); when(mockUnsupportedDevice.isSupported()) .thenReturn(false); when(mockUnsupportedDevice.name).thenReturn('mock-web'); when(mockUnsupportedDevice.id).thenReturn('web-1'); when(mockUnsupportedDevice.targetPlatform).thenAnswer((_) => Future<TargetPlatform>(() => TargetPlatform.web_javascript)); when(mockUnsupportedDevice.isLocalEmulator).thenAnswer((_) => Future<bool>(() => false)); when(mockUnsupportedDevice.sdkNameAndVersion).thenAnswer((_) => Future<String>(() => 'html5')); when(mockDevice.name).thenReturn('mock-android-device'); when(mockDevice.id).thenReturn('mad-28'); when(mockDevice.isSupported()) .thenReturn(true); when(mockDevice.isSupportedForProject(any)) .thenReturn(true); when(mockDevice.targetPlatform).thenAnswer((_) => Future<TargetPlatform>(() => TargetPlatform.android_x64)); when(mockDevice.isLocalEmulator).thenAnswer((_) => Future<bool>(() => false)); when(mockDevice.sdkNameAndVersion).thenAnswer((_) => Future<String>(() => 'sdk-28')); testDeviceManager.addDevice(mockDevice); testDeviceManager.addDevice(mockUnsupportedDevice); final Device device = await findTargetDevice(); expect(device.name, 'mock-android-device'); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Platform: platform, }); } group('findTargetDevice on Linux', () { findTargetDeviceOnOperatingSystem('linux'); }); group('findTargetDevice on Windows', () { findTargetDeviceOnOperatingSystem('windows'); }); group('findTargetDevice on macOS', () { findTargetDeviceOnOperatingSystem('macos'); Platform macOsPlatform() => FakePlatform(operatingSystem: 'macos'); testUsingContext('uses existing simulator', () async { final Device mockDevice = MockDevice(); testDeviceManager.addDevice(mockDevice); when(mockDevice.name).thenReturn('mock-simulator'); when(mockDevice.isLocalEmulator) .thenAnswer((Invocation invocation) => Future<bool>.value(true)); final Device device = await findTargetDevice(); expect(device.name, 'mock-simulator'); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), Platform: macOsPlatform, }); }); group('build arguments', () { String testApp, testFile; setUp(() { restoreAppStarter(); }); Future<Device> appStarterSetup() async { final Device mockDevice = MockDevice(); testDeviceManager.addDevice(mockDevice); final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader(); when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader); final MockLaunchResult mockLaunchResult = MockLaunchResult(); when(mockLaunchResult.started).thenReturn(true); when(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: anyNamed('prebuiltApplication'), )).thenAnswer((_) => Future<LaunchResult>.value(mockLaunchResult)); when(mockDevice.isAppInstalled(any)).thenAnswer((_) => Future<bool>.value(false)); testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); testRunner = (List<String> testArgs, Map<String, String> environment) async { throwToolExit(null, exitCode: 123); }; appStopper = expectAsync1( (DriveCommand command) async { return true; }, count: 2, ); final MemoryFileSystem memFs = fs; await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testFile).writeAsString('main() {}'); return mockDevice; } testUsingContext('does not use pre-built app if no build arg provided', () async { final Device mockDevice = await appStarterSetup(); final List<String> args = <String>[ 'drive', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); } on ToolExit catch (e) { expect(e.exitCode, 123); expect(e.message, null); } verify(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: false, )); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('does not use pre-built app if --build arg provided', () async { final Device mockDevice = await appStarterSetup(); final List<String> args = <String>[ 'drive', '--build', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); } on ToolExit catch (e) { expect(e.exitCode, 123); expect(e.message, null); } verify(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: false, )); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('uses prebuilt app if --no-build arg provided', () async { final Device mockDevice = await appStarterSetup(); final List<String> args = <String>[ 'drive', '--no-build', '--target=$testApp', '--no-pub', ]; try { await createTestCommandRunner(command).run(args); } on ToolExit catch (e) { expect(e.exitCode, 123); expect(e.message, null); } verify(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: true, )); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); }); group('debugging options', () { DebuggingOptions debuggingOptions; String testApp, testFile; setUp(() { restoreAppStarter(); }); Future<Device> appStarterSetup() async { final Device mockDevice = MockDevice(); testDeviceManager.addDevice(mockDevice); final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader(); when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader); final MockLaunchResult mockLaunchResult = MockLaunchResult(); when(mockLaunchResult.started).thenReturn(true); when(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: anyNamed('prebuiltApplication'), )).thenAnswer((Invocation invocation) async { debuggingOptions = invocation.namedArguments[#debuggingOptions] as DebuggingOptions; return mockLaunchResult; }); when(mockDevice.isAppInstalled(any)) .thenAnswer((_) => Future<bool>.value(false)); testApp = globals.fs.path.join(tempDir.path, 'test', 'e2e.dart'); testFile = globals.fs.path.join(tempDir.path, 'test_driver', 'e2e_test.dart'); testRunner = (List<String> testArgs, Map<String, String> environment) async { throwToolExit(null, exitCode: 123); }; appStopper = expectAsync1( (DriveCommand command) async { return true; }, count: 2, ); final MemoryFileSystem memFs = fs; await memFs.file(testApp).writeAsString('main() {}'); await memFs.file(testFile).writeAsString('main() {}'); return mockDevice; } void _testOptionThatDefaultsToFalse( String optionName, bool setToTrue, bool optionValue(), ) { testUsingContext('$optionName ${setToTrue ? 'works' : 'defaults to false'}', () async { final Device mockDevice = await appStarterSetup(); final List<String> args = <String>[ 'drive', '--target=$testApp', if (setToTrue) optionName, '--no-pub', ]; try { await createTestCommandRunner(command).run(args); } on ToolExit catch (e) { expect(e.exitCode, 123); expect(e.message, null); } verify(mockDevice.startApp( null, mainPath: anyNamed('mainPath'), route: anyNamed('route'), debuggingOptions: anyNamed('debuggingOptions'), platformArgs: anyNamed('platformArgs'), prebuiltApplication: false, )); expect(optionValue(), setToTrue ? isTrue : isFalse); }, overrides: <Type, Generator>{ FileSystem: () => fs, ProcessManager: () => FakeProcessManager.any(), }); } void testOptionThatDefaultsToFalse( String optionName, bool optionValue(), ) { _testOptionThatDefaultsToFalse(optionName, true, optionValue); _testOptionThatDefaultsToFalse(optionName, false, optionValue); } testOptionThatDefaultsToFalse( '--dump-skp-on-shader-compilation', () => debuggingOptions.dumpSkpOnShaderCompilation, ); testOptionThatDefaultsToFalse( '--verbose-system-logs', () => debuggingOptions.verboseSystemLogs, ); testOptionThatDefaultsToFalse( '--cache-sksl', () => debuggingOptions.cacheSkSL, ); }); }); group('getDesiredCapabilities', () { test('Chrome with headless on', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'chrome', 'goog:loggingPrefs': <String, String>{ sync_io.LogType.performance: 'ALL'}, 'chromeOptions': <String, dynamic>{ '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', '--headless' ], 'perfLoggingPrefs': <String, String>{ 'traceCategories': 'devtools.timeline,' 'v8,blink.console,benchmark,blink,' 'blink.user_timing' } } }; expect(getDesiredCapabilities(Browser.chrome, true), expected); }); test('Chrome with headless off', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'chrome', 'goog:loggingPrefs': <String, String>{ sync_io.LogType.performance: 'ALL'}, 'chromeOptions': <String, dynamic>{ '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', ], 'perfLoggingPrefs': <String, String>{ 'traceCategories': 'devtools.timeline,' 'v8,blink.console,benchmark,blink,' 'blink.user_timing' } } }; expect(getDesiredCapabilities(Browser.chrome, false), expected); }); test('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); }); test('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); }); test('Edge', () { final Map<String, dynamic> expected = <String, dynamic>{ 'acceptInsecureCerts': true, 'browserName': 'edge', }; expect(getDesiredCapabilities(Browser.edge, false), expected); }); test('macOS Safari', () { final Map<String, dynamic> expected = <String, dynamic>{ 'browserName': 'safari', 'safari.options': <String, dynamic>{ 'skipExtensionInstallation': true, 'cleanSession': true } }; expect(getDesiredCapabilities(Browser.safari, false), expected); }); test('iOS Safari', () { final Map<String, dynamic> expected = <String, dynamic>{ 'platformName': 'ios', 'browserName': 'safari', 'safari:useSimulator': true }; expect(getDesiredCapabilities(Browser.iosSafari, false), expected); }); }); } class MockDevice extends Mock implements Device { MockDevice() { when(isSupported()).thenReturn(true); } } class MockAndroidDevice extends Mock implements AndroidDevice { } class MockLaunchResult extends Mock implements LaunchResult { }