diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 7f04ed36267..ce91d014045 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -332,8 +332,11 @@ class DriveCommand extends RunCommandBase { ); final DriverService driverService = _flutterDriverFactory!.createDriverService(web); final BuildInfo buildInfo = await getBuildInfo(); + final bool isWebDevice = device is ChromiumDevice || device is WebServerDevice; final DebuggingOptions debuggingOptions = await createDebuggingOptions( webDevServerConfig: webDevServerConfig, + webChromeBinary: stringArg('chrome-binary'), + webNoLaunchChrome: isWebDevice, ); final File? applicationBinary = applicationBinaryPath == null ? null @@ -352,7 +355,6 @@ class DriveCommand extends RunCommandBase { mainPath: targetFile, platformArgs: { if (traceStartup) 'trace-startup': traceStartup, - if (web) '--no-launch-chrome': true, }, ); } else { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 451965f6b92..96c41cda253 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -289,7 +289,11 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment /// Create a debugging options instance for the current `run` or `drive` invocation. @visibleForTesting @protected - Future createDebuggingOptions({WebDevServerConfig? webDevServerConfig}) async { + Future createDebuggingOptions({ + WebDevServerConfig? webDevServerConfig, + String? webChromeBinary, + bool webNoLaunchChrome = false, + }) async { final BuildInfo buildInfo = await getBuildInfo(); final int? webBrowserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port') @@ -318,6 +322,8 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, webCrossOriginIsolation: webCrossOriginIsolation, + webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webRenderer: webRenderer, webUseWasm: useWasm, enableImpeller: enableImpeller, @@ -372,6 +378,8 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment webRunHeadless: featureFlags.isWebEnabled && boolArg('web-run-headless'), webBrowserDebugPort: webBrowserDebugPort, webBrowserFlags: webBrowserFlags, + webChromeBinary: webChromeBinary, + webNoLaunchChrome: webNoLaunchChrome, webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'), webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null, diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index f67e69b67c8..893eda055cf 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -971,6 +971,8 @@ class DebuggingOptions { this.webRunHeadless = false, this.webBrowserDebugPort, this.webBrowserFlags = const [], + this.webChromeBinary, + this.webNoLaunchChrome = false, this.webEnableExpressionEvaluation = false, this.webLaunchUrl, bool? webCrossOriginIsolation, @@ -1007,6 +1009,8 @@ class DebuggingOptions { this.webRunHeadless = false, this.webBrowserDebugPort, this.webBrowserFlags = const [], + this.webChromeBinary, + this.webNoLaunchChrome = false, this.webLaunchUrl, bool? webCrossOriginIsolation, WebRendererMode? webRenderer, @@ -1088,6 +1092,8 @@ class DebuggingOptions { required this.webRunHeadless, required this.webBrowserDebugPort, required this.webBrowserFlags, + required this.webChromeBinary, + required this.webNoLaunchChrome, required this.webEnableExpressionEvaluation, required this.webLaunchUrl, required this.webCrossOriginIsolation, @@ -1175,6 +1181,15 @@ class DebuggingOptions { /// Arbitrary browser flags. final List webBrowserFlags; + /// Custom path to the Chrome executable. + final String? webChromeBinary; + + /// Whether to skip launching Chrome when running web apps. + /// + /// This is used by `flutter drive` to prevent launching a second Chrome + /// instance since the driver service launches Chrome separately. + final bool webNoLaunchChrome; + /// Enable expression evaluation for web target. final bool webEnableExpressionEvaluation; @@ -1284,6 +1299,8 @@ class DebuggingOptions { 'webRunHeadless': webRunHeadless, 'webBrowserDebugPort': webBrowserDebugPort, 'webBrowserFlags': webBrowserFlags, + 'webChromeBinary': webChromeBinary, + 'webNoLaunchChrome': webNoLaunchChrome, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'webCrossOriginIsolation': webCrossOriginIsolation, @@ -1353,6 +1370,8 @@ class DebuggingOptions { webRunHeadless: json['webRunHeadless']! as bool, webBrowserDebugPort: json['webBrowserDebugPort'] as int?, webBrowserFlags: (json['webBrowserFlags']! as List).cast(), + webChromeBinary: json['webChromeBinary'] as String?, + webNoLaunchChrome: (json['webNoLaunchChrome'] as bool?) ?? false, webEnableExpressionEvaluation: json['webEnableExpressionEvaluation']! as bool, webLaunchUrl: json['webLaunchUrl'] as String?, webCrossOriginIsolation: json['webCrossOriginIsolation']! as bool, diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart index ef736c51712..56718e8c908 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -86,12 +86,16 @@ class WebDriverService extends DriverService { ? DebuggingOptions.disabled( buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, + webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, ) : DebuggingOptions.enabled( buildInfo, webDevServerConfig: debuggingOptions.webDevServerConfig, + webChromeBinary: debuggingOptions.webChromeBinary, + webNoLaunchChrome: debuggingOptions.webNoLaunchChrome, disablePortPublication: debuggingOptions.disablePortPublication, webRenderer: debuggingOptions.webRenderer, webUseWasm: debuggingOptions.webUseWasm, diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index 78e1c8a2bd2..077d426aab0 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -189,6 +189,7 @@ class ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { if (currentCompleter.isCompleted) { throwToolExit('Only one instance of chrome can be started.'); @@ -200,7 +201,7 @@ class ChromiumLauncher { ); } - final String chromeExecutable = _browserFinder(_platform, _fileSystem); + final String chromeExecutable = chromeBinary ?? _browserFinder(_platform, _fileSystem); if (_logger.isVerbose) { _logger.printTrace('Will use Chromium executable at $chromeExecutable'); diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart index b664e7f44eb..ef13cf85479 100644 --- a/packages/flutter_tools/lib/src/web/web_device.dart +++ b/packages/flutter_tools/lib/src/web/web_device.dart @@ -135,8 +135,7 @@ abstract class ChromiumDevice extends WebDevice { } else { url = platformArgs['uri']! as String; } - final launchChrome = platformArgs['no-launch-chrome'] != true; - if (launchChrome) { + if (!debuggingOptions.webNoLaunchChrome) { _chrome = await chromeLauncher.launch( url, cacheDir: _fileSystem.currentDirectory @@ -145,9 +144,10 @@ abstract class ChromiumDevice extends WebDevice { headless: debuggingOptions.webRunHeadless, debugPort: debuggingOptions.webBrowserDebugPort, webBrowserFlags: debuggingOptions.webBrowserFlags, + chromeBinary: debuggingOptions.webChromeBinary, ); } - _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': launchChrome}); + _logger.sendEvent('app.webLaunchUrl', {'url': url, 'launched': !debuggingOptions.webNoLaunchChrome}); return LaunchResult.succeeded(vmServiceUri: Uri.parse(url)); } diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 8b9727e3d28..fbe973f2794 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -2265,6 +2265,7 @@ class TestChromiumLauncher implements ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { return currentCompleter.future; } diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart index 088bcc0a007..6743b72e9a1 100644 --- a/packages/flutter_tools/test/general.shard/web/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart @@ -58,6 +58,52 @@ void main() { }, ); + testWithoutContext( + 'ChromiumDevice.startApp does not launch Chrome when webNoLaunchChrome is true', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: true, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isFalse); + }, + ); + + testWithoutContext( + 'ChromiumDevice.startApp launches Chrome when webNoLaunchChrome is false', + () async { + final launcher = _TrackingChromiumLauncher(); + final chromiumDevice = _FakeChromiumDevice( + chromiumLauncher: launcher, + fileSystem: MemoryFileSystem.test(), + logger: BufferLogger.test(), + ); + + await chromiumDevice.startApp( + null, + debuggingOptions: DebuggingOptions.enabled( + BuildInfo.debug, + webNoLaunchChrome: false, + ), + platformArgs: {'uri': 'http://localhost:8080'}, + ); + + expect(launcher.launchCalled, isTrue); + }, + ); + testWithoutContext('GoogleChromeDevice defaults', () async { final launcher = TestChromiumLauncher(); @@ -456,6 +502,7 @@ class TestChromiumLauncher implements ChromiumLauncher { bool skipCheck = false, Directory? cacheDir, List webBrowserFlags = const [], + String? chromeBinary, }) async { currentCompleter.complete(_launcher()); return currentCompleter.future; @@ -494,3 +541,44 @@ class _OnceClosableChromium extends Fake implements Chromium { } class _UnimplementedChromium extends Fake implements Chromium {} + +/// A test implementation of [ChromiumLauncher] that tracks whether launch() was called. +class _TrackingChromiumLauncher implements ChromiumLauncher { + bool launchCalled = false; + + @override + Completer currentCompleter = Completer(); + + @override + bool canFindExecutable() => true; + + @override + Future get connectedInstance => currentCompleter.future; + + @override + String findExecutable() => 'chrome'; + + @override + bool get hasChromeInstance => false; + + @override + Future launch( + String url, { + bool headless = false, + int? debugPort, + bool skipCheck = false, + Directory? cacheDir, + List webBrowserFlags = const [], + String? chromeBinary, + }) async { + launchCalled = true; + final chromium = _UnimplementedChromium(); + currentCompleter.complete(chromium); + return chromium; + } + + @override + Future connect(Chromium chrome, bool skipCheck) { + return currentCompleter.future; + } +} diff --git a/packages/flutter_tools/test/web.shard/chrome_test.dart b/packages/flutter_tools/test/web.shard/chrome_test.dart index c80f0bae3a6..2f9ad4e46ab 100644 --- a/packages/flutter_tools/test/web.shard/chrome_test.dart +++ b/packages/flutter_tools/test/web.shard/chrome_test.dart @@ -527,6 +527,29 @@ void main() { ); }); + testWithoutContext('can launch chrome with custom binary path', () async { + processManager.addCommand( + const FakeCommand( + command: [ + '/custom/path/to/chrome', + '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0', + '--remote-debugging-port=12345', + ...kChromeArgs, + 'example_url', + ], + stderr: kDevtoolsStderr, + ), + ); + + await expectReturnsNormallyLater( + chromeLauncher.launch( + 'example_url', + skipCheck: true, + chromeBinary: '/custom/path/to/chrome', + ), + ); + }); + testWithoutContext('can launch chrome headless', () async { processManager.addCommand( const FakeCommand(