fix: prevent duplicate Chrome launch in flutter drive

flutter drive -d chrome was opening Chrome twice - once from WebDriver
and once from web_device.dart. Added webNoLaunchChrome flag to
DebuggingOptions so we can skip the second launch.
This commit is contained in:
shindonghwi 2026-02-01 15:37:48 +09:00
parent b3993b944e
commit 2fae58b08f
6 changed files with 107 additions and 4 deletions

View File

@ -332,9 +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
@ -353,7 +355,6 @@ class DriveCommand extends RunCommandBase {
mainPath: targetFile,
platformArgs: <String, Object>{
if (traceStartup) 'trace-startup': traceStartup,
if (web) '--no-launch-chrome': true,
},
);
} else {

View File

@ -285,6 +285,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
Future<DebuggingOptions> createDebuggingOptions({
WebDevServerConfig? webDevServerConfig,
String? webChromeBinary,
bool webNoLaunchChrome = false,
}) async {
final BuildInfo buildInfo = await getBuildInfo();
final int? webBrowserDebugPort =
@ -315,6 +316,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
webBrowserFlags: webBrowserFlags,
webCrossOriginIsolation: webCrossOriginIsolation,
webChromeBinary: webChromeBinary,
webNoLaunchChrome: webNoLaunchChrome,
webRenderer: webRenderer,
webUseWasm: useWasm,
enableImpeller: enableImpeller,
@ -369,6 +371,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
webBrowserDebugPort: webBrowserDebugPort,
webBrowserFlags: webBrowserFlags,
webChromeBinary: webChromeBinary,
webNoLaunchChrome: webNoLaunchChrome,
webEnableExpressionEvaluation:
featureFlags.isWebEnabled && boolArg('web-enable-expression-evaluation'),
webLaunchUrl: featureFlags.isWebEnabled ? stringArg('web-launch-url') : null,

View File

@ -986,6 +986,7 @@ class DebuggingOptions {
this.webBrowserDebugPort,
this.webBrowserFlags = const <String>[],
this.webChromeBinary,
this.webNoLaunchChrome = false,
this.webEnableExpressionEvaluation = false,
this.webLaunchUrl,
bool? webCrossOriginIsolation,
@ -1022,6 +1023,7 @@ class DebuggingOptions {
this.webBrowserDebugPort,
this.webBrowserFlags = const <String>[],
this.webChromeBinary,
this.webNoLaunchChrome = false,
this.webLaunchUrl,
bool? webCrossOriginIsolation,
WebRendererMode? webRenderer,
@ -1103,6 +1105,7 @@ class DebuggingOptions {
required this.webBrowserDebugPort,
required this.webBrowserFlags,
required this.webChromeBinary,
required this.webNoLaunchChrome,
required this.webEnableExpressionEvaluation,
required this.webLaunchUrl,
required this.webCrossOriginIsolation,
@ -1191,6 +1194,12 @@ class DebuggingOptions {
/// 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;
@ -1301,6 +1310,7 @@ class DebuggingOptions {
'webBrowserDebugPort': webBrowserDebugPort,
'webBrowserFlags': webBrowserFlags,
'webChromeBinary': webChromeBinary,
'webNoLaunchChrome': webNoLaunchChrome,
'webEnableExpressionEvaluation': webEnableExpressionEvaluation,
'webLaunchUrl': webLaunchUrl,
'webCrossOriginIsolation': webCrossOriginIsolation,
@ -1370,6 +1380,7 @@ class DebuggingOptions {
webBrowserDebugPort: json['webBrowserDebugPort'] as int?,
webBrowserFlags: (json['webBrowserFlags']! as List<dynamic>).cast<String>(),
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,

View File

@ -87,6 +87,7 @@ class WebDriverService extends DriverService {
buildInfo,
webDevServerConfig: debuggingOptions.webDevServerConfig,
webChromeBinary: debuggingOptions.webChromeBinary,
webNoLaunchChrome: debuggingOptions.webNoLaunchChrome,
webRenderer: debuggingOptions.webRenderer,
webUseWasm: debuggingOptions.webUseWasm,
)
@ -94,6 +95,7 @@ class WebDriverService extends DriverService {
buildInfo,
webDevServerConfig: debuggingOptions.webDevServerConfig,
webChromeBinary: debuggingOptions.webChromeBinary,
webNoLaunchChrome: debuggingOptions.webNoLaunchChrome,
disablePortPublication: debuggingOptions.disablePortPublication,
webRenderer: debuggingOptions.webRenderer,
webUseWasm: debuggingOptions.webUseWasm,

View File

@ -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
@ -148,7 +147,7 @@ abstract class ChromiumDevice extends WebDevice {
chromeBinary: debuggingOptions.webChromeBinary,
);
}
_logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': launchChrome});
_logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': !debuggingOptions.webNoLaunchChrome});
return LaunchResult.succeeded(vmServiceUri: Uri.parse(url));
}

View File

@ -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: <String, Object?>{'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: <String, Object?>{'uri': 'http://localhost:8080'},
);
expect(launcher.launchCalled, isTrue);
},
);
testWithoutContext('GoogleChromeDevice defaults', () async {
final launcher = TestChromiumLauncher();
@ -495,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<Chromium> currentCompleter = Completer<Chromium>();
@override
bool canFindExecutable() => true;
@override
Future<Chromium> get connectedInstance => currentCompleter.future;
@override
String findExecutable() => 'chrome';
@override
bool get hasChromeInstance => false;
@override
Future<Chromium> launch(
String url, {
bool headless = false,
int? debugPort,
bool skipCheck = false,
Directory? cacheDir,
List<String> webBrowserFlags = const <String>[],
String? chromeBinary,
}) async {
launchCalled = true;
final chromium = _UnimplementedChromium();
currentCompleter.complete(chromium);
return chromium;
}
@override
Future<Chromium> connect(Chromium chrome, bool skipCheck) {
return currentCompleter.future;
}
}