feat(tools): Arbitrary browser flags (closes #65575) (#104935)
diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index ff1d1a5..3e4ca94 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -279,6 +279,7 @@
packageConfig,
chromeBinary: stringArgDeprecated('chrome-binary'),
headless: boolArgDeprecated('headless'),
+ webBrowserFlags: stringsArg(FlutterOptions.kWebBrowserFlag),
browserDimension: stringArgDeprecated('browser-dimension')!.split(','),
browserName: stringArgDeprecated('browser-name'),
driverPort: stringArgDeprecated('driver-port') != null
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index d986f7c..8b05c2c 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -202,9 +202,12 @@
@protected
Future<DebuggingOptions> createDebuggingOptions(bool webMode) async {
final BuildInfo buildInfo = await getBuildInfo();
- final int? browserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port')
+ final int? webBrowserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port')
? int.parse(stringArgDeprecated('web-browser-debug-port')!)
: null;
+ final List<String> webBrowserFlags = featureFlags.isWebEnabled
+ ? stringsArg(FlutterOptions.kWebBrowserFlag)
+ : const <String>[];
if (buildInfo.mode.isRelease) {
return DebuggingOptions.disabled(
buildInfo,
@@ -216,7 +219,8 @@
webUseSseForInjectedClient: featureFlags.isWebEnabled && stringArgDeprecated('web-server-debug-injected-client-protocol') == 'sse',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArgDeprecated('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArgDeprecated('web-run-headless'),
- webBrowserDebugPort: browserDebugPort,
+ webBrowserDebugPort: webBrowserDebugPort,
+ webBrowserFlags: webBrowserFlags,
enableImpeller: enableImpeller,
uninstallFirst: uninstallFirst,
);
@@ -253,7 +257,8 @@
webUseSseForInjectedClient: featureFlags.isWebEnabled && stringArgDeprecated('web-server-debug-injected-client-protocol') == 'sse',
webEnableExposeUrl: featureFlags.isWebEnabled && boolArgDeprecated('web-allow-expose-url'),
webRunHeadless: featureFlags.isWebEnabled && boolArgDeprecated('web-run-headless'),
- webBrowserDebugPort: browserDebugPort,
+ webBrowserDebugPort: webBrowserDebugPort,
+ webBrowserFlags: webBrowserFlags,
webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArgDeprecated('web-enable-expression-evaluation'),
webLaunchUrl: featureFlags.isWebEnabled ? stringArgDeprecated('web-launch-url') : null,
vmserviceOutFile: stringArgDeprecated('vmservice-out-file'),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 4fbcc64..9eeef33 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -785,6 +785,7 @@
this.webUseSseForInjectedClient = true,
this.webRunHeadless = false,
this.webBrowserDebugPort,
+ this.webBrowserFlags = const <String>[],
this.webEnableExpressionEvaluation = false,
this.webLaunchUrl,
this.vmserviceOutFile,
@@ -805,6 +806,7 @@
this.webUseSseForInjectedClient = true,
this.webRunHeadless = false,
this.webBrowserDebugPort,
+ this.webBrowserFlags = const <String>[],
this.webLaunchUrl,
this.cacheSkSL = false,
this.traceAllowlist,
@@ -871,6 +873,7 @@
required this.webUseSseForInjectedClient,
required this.webRunHeadless,
required this.webBrowserDebugPort,
+ required this.webBrowserFlags,
required this.webEnableExpressionEvaluation,
required this.webLaunchUrl,
required this.vmserviceOutFile,
@@ -930,6 +933,9 @@
/// The port the browser should use for its debugging protocol.
final int? webBrowserDebugPort;
+ /// Arbitrary browser flags.
+ final List<String> webBrowserFlags;
+
/// Enable expression evaluation for web target.
final bool webEnableExpressionEvaluation;
@@ -983,6 +989,7 @@
'webUseSseForInjectedClient': webUseSseForInjectedClient,
'webRunHeadless': webRunHeadless,
'webBrowserDebugPort': webBrowserDebugPort,
+ 'webBrowserFlags': webBrowserFlags,
'webEnableExpressionEvaluation': webEnableExpressionEvaluation,
'webLaunchUrl': webLaunchUrl,
'vmserviceOutFile': vmserviceOutFile,
@@ -1027,6 +1034,7 @@
webUseSseForInjectedClient: (json['webUseSseForInjectedClient'] as bool?)!,
webRunHeadless: (json['webRunHeadless'] as bool?)!,
webBrowserDebugPort: json['webBrowserDebugPort'] as int?,
+ webBrowserFlags: ((json['webBrowserFlags'] as List<dynamic>?)?.cast<String>())!,
webEnableExpressionEvaluation: (json['webEnableExpressionEvaluation'] as bool?)!,
webLaunchUrl: json['webLaunchUrl'] as String?,
vmserviceOutFile: json['vmserviceOutFile'] as String?,
diff --git a/packages/flutter_tools/lib/src/drive/drive_service.dart b/packages/flutter_tools/lib/src/drive/drive_service.dart
index 756c6ec..91f0560 100644
--- a/packages/flutter_tools/lib/src/drive/drive_service.dart
+++ b/packages/flutter_tools/lib/src/drive/drive_service.dart
@@ -97,6 +97,7 @@
String? browserName,
bool? androidEmulator,
int? driverPort,
+ List<String> webBrowserFlags,
List<String>? browserDimension,
String? profileMemory,
});
@@ -254,6 +255,7 @@
String? browserName,
bool? androidEmulator,
int? driverPort,
+ List<String> webBrowserFlags = const <String>[],
List<String>? browserDimension,
String? profileMemory,
}) async {
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 e686a08..2df3709 100644
--- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart
+++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart
@@ -136,6 +136,7 @@
String? browserName,
bool? androidEmulator,
int? driverPort,
+ List<String> webBrowserFlags = const <String>[],
List<String>? browserDimension,
String? profileMemory,
}) async {
@@ -144,7 +145,12 @@
try {
webDriver = await async_io.createDriver(
uri: Uri.parse('http://localhost:$driverPort/'),
- desired: getDesiredCapabilities(browser, headless, chromeBinary),
+ desired: getDesiredCapabilities(
+ browser,
+ headless,
+ webBrowserFlags: webBrowserFlags,
+ chromeBinary: chromeBinary,
+ ),
);
} on SocketException catch (error) {
_logger.printTrace('$error');
@@ -234,10 +240,15 @@
safari,
}
-/// Returns desired capabilities for given [browser], [headless] and
-/// [chromeBinary].
+/// Returns desired capabilities for given [browser], [headless], [chromeBinary]
+/// and [webBrowserFlags].
@visibleForTesting
-Map<String, dynamic> getDesiredCapabilities(Browser browser, bool? headless, [String? chromeBinary]) {
+Map<String, dynamic> getDesiredCapabilities(
+ Browser browser,
+ bool? headless, {
+ List<String> webBrowserFlags = const <String>[],
+ String? chromeBinary,
+}) {
switch (browser) {
case Browser.chrome:
return <String, dynamic>{
@@ -262,6 +273,7 @@
'--no-sandbox',
'--no-first-run',
if (headless!) '--headless',
+ ...webBrowserFlags,
],
'perfLoggingPrefs': <String, String>{
'traceCategories':
@@ -278,6 +290,7 @@
'moz:firefoxOptions' : <String, dynamic>{
'args': <String>[
if (headless!) '-headless',
+ ...webBrowserFlags,
],
'prefs': <String, dynamic>{
'dom.file.createInChild': true,
@@ -313,7 +326,10 @@
'platformName': 'android',
'goog:chromeOptions': <String, dynamic>{
'androidPackage': 'com.android.chrome',
- 'args': <String>['--disable-fullscreen'],
+ 'args': <String>[
+ '--disable-fullscreen',
+ ...webBrowserFlags,
+ ],
},
};
}
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index f48d084..c2f8d59 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -119,6 +119,7 @@
static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date';
static const String kFatalWarnings = 'fatal-warnings';
static const String kUseApplicationBinary = 'use-application-binary';
+ static const String kWebBrowserFlag = 'web-browser-flag';
}
/// flutter command categories for usage.
@@ -270,6 +271,15 @@
help: 'The URL to provide to the browser. Defaults to an HTTP URL with the host '
'name of "--web-hostname", the port of "--web-port", and the path set to "/".',
);
+ argParser.addMultiOption(
+ FlutterOptions.kWebBrowserFlag,
+ help: 'Additional flag to pass to a browser instance at startup.\n'
+ 'Chrome: https://www.chromium.org/developers/how-tos/run-chromium-with-flags/\n'
+ 'Firefox: https://wiki.mozilla.org/Firefox/CommandLineOptions\n'
+ 'Multiple flags can be passed by repeating "--${FlutterOptions.kWebBrowserFlag}" multiple times.',
+ valueHelp: '--foo=bar',
+ hide: !verboseHelp,
+ );
}
void usesTargetOption() {
diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
index fc20e51..587a2c5 100644
--- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
@@ -659,6 +659,8 @@
///
/// The browser will start in headless mode if [headless] is true.
///
+ /// Add arbitrary browser flags via [webBrowserFlags].
+ ///
/// The [settings] indicate how to invoke this browser's executable.
///
/// Returns the browser manager, or throws an [ApplicationException] if a
@@ -670,8 +672,13 @@
Future<WebSocketChannel> future, {
bool debug = false,
bool headless = true,
+ List<String> webBrowserFlags = const <String>[],
}) async {
- final Chromium chrome = await chromiumLauncher.launch(url.toString(), headless: headless);
+ final Chromium chrome = await chromiumLauncher.launch(
+ url.toString(),
+ headless: headless,
+ webBrowserFlags: webBrowserFlags,
+ );
final Completer<BrowserManager> completer = Completer<BrowserManager>();
unawaited(chrome.onExit.then((int? browserExitCode) {
diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart
index 0984d09..43a1e2f 100644
--- a/packages/flutter_tools/lib/src/web/chrome.dart
+++ b/packages/flutter_tools/lib/src/web/chrome.dart
@@ -164,11 +164,14 @@
/// port is picked automatically.
///
/// [skipCheck] does not attempt to make a devtools connection before returning.
+ ///
+ /// [webBrowserFlags] add arbitrary browser flags.
Future<Chromium> launch(String url, {
bool headless = false,
int? debugPort,
bool skipCheck = false,
Directory? cacheDir,
+ List<String> webBrowserFlags = const <String>[],
}) async {
if (currentCompleter.isCompleted) {
throwToolExit('Only one instance of chrome can be started.');
@@ -215,6 +218,7 @@
'--no-sandbox',
'--window-size=2400,1800',
],
+ ...webBrowserFlags,
url,
];
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 902ab99..1482686 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -149,6 +149,7 @@
.childDirectory('chrome-device'),
headless: debuggingOptions.webRunHeadless,
debugPort: debuggingOptions.webBrowserDebugPort,
+ webBrowserFlags: debuggingOptions.webBrowserFlags,
);
}
_logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': launchChrome});
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
index 1da0451..529e76c 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
@@ -354,6 +354,7 @@
String browserName,
bool androidEmulator,
int driverPort,
+ List<String> webBrowserFlags,
List<String> browserDimension,
String profileMemory,
}) async => 1;
diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
index dad555c..03a4773 100644
--- a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
+++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
@@ -24,6 +24,18 @@
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>{
@@ -36,15 +48,7 @@
'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',
+ ...kChromeArgs,
'--headless',
],
'perfLoggingPrefs': <String, String>{
@@ -71,17 +75,7 @@
'chromeOptions': <String, dynamic>{
'binary': chromeBinary,
'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',
- ],
+ 'args': kChromeArgs,
'perfLoggingPrefs': <String, String>{
'traceCategories':
'devtools.timeline,'
@@ -91,10 +85,43 @@
},
};
- expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary), expected);
+ 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',
+ },
+ 'chromeOptions': <String, dynamic>{
+ 'w3c': false,
+ '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,
@@ -141,6 +168,36 @@
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,
@@ -169,16 +226,24 @@
});
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'],
+ 'args': <String>[
+ '--disable-fullscreen',
+ '--autoplay-policy=no-user-gesture-required',
+ '--incognito',
+ ],
},
};
- expect(getDesiredCapabilities(Browser.androidChrome, false), expected);
+ expect(getDesiredCapabilities(Browser.androidChrome, false, webBrowserFlags: webBrowserFlags), expected);
});
testUsingContext('WebDriverService starts and stops an app', () async {
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 00f08a2..6eccf1a 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
@@ -1307,7 +1307,14 @@
bool get hasChromeInstance => _hasInstance;
@override
- Future<Chromium> launch(String url, {bool headless = false, int debugPort, bool skipCheck = false, Directory cacheDir}) async {
+ Future<Chromium> launch(
+ String url, {
+ bool headless = false,
+ int debugPort,
+ bool skipCheck = false,
+ Directory cacheDir,
+ List<String> webBrowserFlags = const <String>[],
+ }) 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 17c5b3a..270da31 100644
--- a/packages/flutter_tools/test/general.shard/web/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart
@@ -389,7 +389,14 @@
bool get hasChromeInstance => _hasInstance;
@override
- Future<Chromium> launch(String url, {bool headless = false, int? debugPort, bool skipCheck = false, Directory? cacheDir}) async {
+ Future<Chromium> launch(
+ String url, {
+ bool headless = false,
+ int? debugPort,
+ bool skipCheck = false,
+ Directory? cacheDir,
+ List<String> webBrowserFlags = const <String>[],
+ }) async {
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 9cd9cb3..378a699 100644
--- a/packages/flutter_tools/test/web.shard/chrome_test.dart
+++ b/packages/flutter_tools/test/web.shard/chrome_test.dart
@@ -328,6 +328,32 @@
);
});
+ testWithoutContext('can launch chrome with arbitrary flags', () async {
+ processManager.addCommand(const FakeCommand(
+ command: <String>[
+ 'example_chrome',
+ '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
+ '--remote-debugging-port=12345',
+ ...kChromeArgs,
+ '--autoplay-policy=no-user-gesture-required',
+ '--incognito',
+ '--auto-select-desktop-capture-source="Entire screen"',
+ 'example_url',
+ ],
+ stderr: kDevtoolsStderr,
+ ));
+
+ await expectReturnsNormallyLater(chromeLauncher.launch(
+ 'example_url',
+ skipCheck: true,
+ webBrowserFlags: <String>[
+ '--autoplay-policy=no-user-gesture-required',
+ '--incognito',
+ '--auto-select-desktop-capture-source="Entire screen"',
+ ],
+ ));
+ });
+
testWithoutContext('can launch chrome headless', () async {
processManager.addCommand(const FakeCommand(
command: <String>[