diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index ff1d1a5d4901..3e4ca94fe2f8 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -279,6 +279,7 @@ class DriveCommand extends RunCommandBase { 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 d986f7cdb7ae..8b05c2c3fa0b 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -202,9 +202,12 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment @protected Future 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 webBrowserFlags = featureFlags.isWebEnabled + ? stringsArg(FlutterOptions.kWebBrowserFlag) + : const []; if (buildInfo.mode.isRelease) { return DebuggingOptions.disabled( buildInfo, @@ -216,7 +219,8 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment 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 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment 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 4fbcc64bb5ad..9eeef33db1c9 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -785,6 +785,7 @@ class DebuggingOptions { this.webUseSseForInjectedClient = true, this.webRunHeadless = false, this.webBrowserDebugPort, + this.webBrowserFlags = const [], this.webEnableExpressionEvaluation = false, this.webLaunchUrl, this.vmserviceOutFile, @@ -805,6 +806,7 @@ class DebuggingOptions { this.webUseSseForInjectedClient = true, this.webRunHeadless = false, this.webBrowserDebugPort, + this.webBrowserFlags = const [], this.webLaunchUrl, this.cacheSkSL = false, this.traceAllowlist, @@ -871,6 +873,7 @@ class DebuggingOptions { 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 @@ class DebuggingOptions { /// The port the browser should use for its debugging protocol. final int? webBrowserDebugPort; + /// Arbitrary browser flags. + final List webBrowserFlags; + /// Enable expression evaluation for web target. final bool webEnableExpressionEvaluation; @@ -983,6 +989,7 @@ class DebuggingOptions { 'webUseSseForInjectedClient': webUseSseForInjectedClient, 'webRunHeadless': webRunHeadless, 'webBrowserDebugPort': webBrowserDebugPort, + 'webBrowserFlags': webBrowserFlags, 'webEnableExpressionEvaluation': webEnableExpressionEvaluation, 'webLaunchUrl': webLaunchUrl, 'vmserviceOutFile': vmserviceOutFile, @@ -1027,6 +1034,7 @@ class DebuggingOptions { webUseSseForInjectedClient: (json['webUseSseForInjectedClient'] as bool?)!, webRunHeadless: (json['webRunHeadless'] as bool?)!, webBrowserDebugPort: json['webBrowserDebugPort'] as int?, + webBrowserFlags: ((json['webBrowserFlags'] as List?)?.cast())!, 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 756c6ecc7b20..91f0560eea32 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 @@ abstract class DriverService { String? browserName, bool? androidEmulator, int? driverPort, + List webBrowserFlags, List? browserDimension, String? profileMemory, }); @@ -254,6 +255,7 @@ class FlutterDriverService extends DriverService { String? browserName, bool? androidEmulator, int? driverPort, + List webBrowserFlags = const [], List? 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 e686a08ffb03..2df370913ea7 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 @@ class WebDriverService extends DriverService { String? browserName, bool? androidEmulator, int? driverPort, + List webBrowserFlags = const [], List? browserDimension, String? profileMemory, }) async { @@ -144,7 +145,12 @@ class WebDriverService extends DriverService { 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 @@ enum Browser { safari, } -/// Returns desired capabilities for given [browser], [headless] and -/// [chromeBinary]. +/// Returns desired capabilities for given [browser], [headless], [chromeBinary] +/// and [webBrowserFlags]. @visibleForTesting -Map getDesiredCapabilities(Browser browser, bool? headless, [String? chromeBinary]) { +Map getDesiredCapabilities( + Browser browser, + bool? headless, { + List webBrowserFlags = const [], + String? chromeBinary, +}) { switch (browser) { case Browser.chrome: return { @@ -262,6 +273,7 @@ Map getDesiredCapabilities(Browser browser, bool? headless, [St '--no-sandbox', '--no-first-run', if (headless!) '--headless', + ...webBrowserFlags, ], 'perfLoggingPrefs': { 'traceCategories': @@ -278,6 +290,7 @@ Map getDesiredCapabilities(Browser browser, bool? headless, [St 'moz:firefoxOptions' : { 'args': [ if (headless!) '-headless', + ...webBrowserFlags, ], 'prefs': { 'dom.file.createInChild': true, @@ -313,7 +326,10 @@ Map getDesiredCapabilities(Browser browser, bool? headless, [St 'platformName': 'android', 'goog:chromeOptions': { 'androidPackage': 'com.android.chrome', - 'args': ['--disable-fullscreen'], + 'args': [ + '--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 f48d0840abad..c2f8d592768f 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 @@ class FlutterOptions { 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 @@ abstract class FlutterCommand extends Command { 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 fc20e51fa7ad..587a2c51708b 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 @@ class BrowserManager { /// /// 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 @@ class BrowserManager { Future future, { bool debug = false, bool headless = true, + List webBrowserFlags = const [], }) 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 completer = Completer(); 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 0984d093fd19..43a1e2f60e6e 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -164,11 +164,14 @@ class ChromiumLauncher { /// port is picked automatically. /// /// [skipCheck] does not attempt to make a devtools connection before returning. + /// + /// [webBrowserFlags] add arbitrary browser flags. Future launch(String url, { bool headless = false, int? debugPort, bool skipCheck = false, Directory? cacheDir, + List webBrowserFlags = const [], }) async { if (currentCompleter.isCompleted) { throwToolExit('Only one instance of chrome can be started.'); @@ -215,6 +218,7 @@ class ChromiumLauncher { '--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 902ab99c2e3c..1482686ea06d 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 @@ abstract class ChromiumDevice extends Device { .childDirectory('chrome-device'), headless: debuggingOptions.webRunHeadless, debugPort: debuggingOptions.webBrowserDebugPort, + webBrowserFlags: debuggingOptions.webBrowserFlags, ); } _logger.sendEvent('app.webLaunchUrl', {'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 1da04514e50c..529e76cf03cb 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 @@ class FailingFakeDriverService extends Fake implements DriverService { String browserName, bool androidEmulator, int driverPort, + List webBrowserFlags, List 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 dad555c13821..03a4773413e2 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 'package:webdriver/sync_io.dart' as sync_io; import '../../src/common.dart'; import '../../src/context.dart'; +const List kChromeArgs = [ + '--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 expected = { @@ -36,15 +48,7 @@ void main() { 'chromeOptions': { 'w3c': false, 'args': [ - '--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': { @@ -71,17 +75,7 @@ void main() { 'chromeOptions': { 'binary': chromeBinary, 'w3c': false, - 'args': [ - '--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': { 'traceCategories': 'devtools.timeline,' @@ -91,8 +85,41 @@ void main() { }, }; - expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary), expected); + expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary: chromeBinary), expected); + + }); + + testWithoutContext('getDesiredCapabilities Chrome with browser flags', () { + const List webBrowserFlags = [ + '--autoplay-policy=no-user-gesture-required', + '--incognito', + '--auto-select-desktop-capture-source="Entire screen"', + ]; + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'chrome', + 'goog:loggingPrefs': { + sync_io.LogType.browser: 'INFO', + sync_io.LogType.performance: 'ALL', + }, + 'chromeOptions': { + 'w3c': false, + 'args': [ + ...kChromeArgs, + '--autoplay-policy=no-user-gesture-required', + '--incognito', + '--auto-select-desktop-capture-source="Entire screen"', + ], + 'perfLoggingPrefs': { + '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', () { @@ -141,6 +168,36 @@ void main() { expect(getDesiredCapabilities(Browser.firefox, false), expected); }); + testWithoutContext('getDesiredCapabilities Firefox with browser flags', () { + const List webBrowserFlags = [ + '-url=https://example.com', + '-private', + ]; + final Map expected = { + 'acceptInsecureCerts': true, + 'browserName': 'firefox', + 'moz:firefoxOptions' : { + 'args': [ + '-url=https://example.com', + '-private', + ], + 'prefs': { + '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': {'level': 'trace'}, + }, + }; + + expect(getDesiredCapabilities(Browser.firefox, false, webBrowserFlags: webBrowserFlags), expected); + }); + testWithoutContext('getDesiredCapabilities Edge', () { final Map expected = { 'acceptInsecureCerts': true, @@ -169,16 +226,24 @@ void main() { }); testWithoutContext('getDesiredCapabilities android chrome', () { + const List webBrowserFlags = [ + '--autoplay-policy=no-user-gesture-required', + '--incognito', + ]; final Map expected = { 'browserName': 'chrome', 'platformName': 'android', 'goog:chromeOptions': { 'androidPackage': 'com.android.chrome', - 'args': ['--disable-fullscreen'], + 'args': [ + '--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 00f08a257f3b..6eccf1abf6a5 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 @@ class TestChromiumLauncher implements ChromiumLauncher { bool get hasChromeInstance => _hasInstance; @override - Future launch(String url, {bool headless = false, int debugPort, bool skipCheck = false, Directory cacheDir}) async { + Future launch( + String url, { + bool headless = false, + int debugPort, + bool skipCheck = false, + Directory cacheDir, + List webBrowserFlags = const [], + }) 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 17c5b3a95c10..270da312a25c 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 @@ class TestChromiumLauncher implements ChromiumLauncher { bool get hasChromeInstance => _hasInstance; @override - Future launch(String url, {bool headless = false, int? debugPort, bool skipCheck = false, Directory? cacheDir}) async { + Future launch( + String url, { + bool headless = false, + int? debugPort, + bool skipCheck = false, + Directory? cacheDir, + List webBrowserFlags = const [], + }) 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 9cd9cb38ee0d..378a699a8866 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 @@ void main() { ); }); + testWithoutContext('can launch chrome with arbitrary flags', () async { + processManager.addCommand(const FakeCommand( + command: [ + '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: [ + '--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: [