Merge 2fae58b08f4e0b1ef1e4e6299d4f1a496804f129 into 06df71c51446e96939c6a615b7c34ce9123806ba

This commit is contained in:
shindonghwi 2026-02-19 15:49:00 +00:00 committed by GitHub
commit 78011d9502
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 152 additions and 6 deletions

View File

@ -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: <String, Object>{
if (traceStartup) 'trace-startup': traceStartup,
if (web) '--no-launch-chrome': true,
},
);
} else {

View File

@ -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<DebuggingOptions> createDebuggingOptions({WebDevServerConfig? webDevServerConfig}) async {
Future<DebuggingOptions> 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,

View File

@ -971,6 +971,8 @@ class DebuggingOptions {
this.webRunHeadless = false,
this.webBrowserDebugPort,
this.webBrowserFlags = const <String>[],
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 <String>[],
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<String> 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<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

@ -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,

View File

@ -189,6 +189,7 @@ class ChromiumLauncher {
bool skipCheck = false,
Directory? cacheDir,
List<String> webBrowserFlags = const <String>[],
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');

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
@ -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', <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

@ -2265,6 +2265,7 @@ class TestChromiumLauncher implements ChromiumLauncher {
bool skipCheck = false,
Directory? cacheDir,
List<String> webBrowserFlags = const <String>[],
String? chromeBinary,
}) async {
return currentCompleter.future;
}

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();
@ -456,6 +502,7 @@ class TestChromiumLauncher implements ChromiumLauncher {
bool skipCheck = false,
Directory? cacheDir,
List<String> webBrowserFlags = const <String>[],
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<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;
}
}

View File

@ -527,6 +527,29 @@ void main() {
);
});
testWithoutContext('can launch chrome with custom binary path', () async {
processManager.addCommand(
const FakeCommand(
command: <String>[
'/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(